diff --git a/README.md b/README.md index 80082a55b5..79ee88c157 100644 --- a/README.md +++ b/README.md @@ -1084,3 +1084,35 @@ If you see errors like `Cannot find module '@ledgerhq/hw-transport-node-hid'` or - Open the appropriate app (Ethereum or Solana) on the device - Try different USB ports or cables - Close Ledger Live if it's running (it may lock the device) + +## Running in Debug (vscode launch.json configurations - tested on Ubuntu) +- Run single test file outside project (eg. import Chain or Connector and .init()) + { + "type": "node", + "request": "launch", + "name": "BUILD/DEBUG SINGLE FILE: src/testnojest.ts +externalTerminal", + "program": "${workspaceFolder}/dist/osmosis.testnojest.js", + "preLaunchTask": "npm: build", + "args": ["--passphrase=PASSPHRASE", "--dev"], + "env":{"START_SERVER":"true", "GATEWAY_TEST_MODE":"dev"}, + "internalConsoleOptions": "openOnSessionStart", + "console": "externalTerminal", + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ] + }, +- Run whole project in debug mode (breakpoints work on TS files) + { + "type": "node", + "request": "launch", + "name": "BUILD/DEBUG FULL: index.js +externalTerminal for info()", + "console": "externalTerminal", + "program": "${workspaceFolder}/dist/index.js", + "preLaunchTask": "npm: build", + "args": ["--passphrase=PASSPHRASE", "--dev"], + "env":{"START_SERVER":"true", "GATEWAY_TEST_MODE":"dev"}, + "internalConsoleOptions": "openOnSessionStart", + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ] + }, \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 58a19a83e7..aaaf6c2be6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,9 +29,11 @@ module.exports = { testMatch: ['/test/**/*.test.ts', '/test/**/*.test.js'], transform: { '^.+\\.tsx?$': 'ts-jest', + "node_modules/@scure/.*.(j|t)sx?$": "ts-jest" }, - transformIgnorePatterns: ['/node_modules/(?!.*superjson)'], + transformIgnorePatterns: [], //'/node_modules/(?!.*superjson)'], moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/', + useESM:true, }), }; diff --git a/openapi.json b/openapi.json index 36522e3fc9..61b75b2c82 100644 --- a/openapi.json +++ b/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Hummingbot Gateway", "description": "API endpoints for interacting with DEXs and blockchains", - "version": "dev-2.8.0" + "version": "dev-2.11.0" }, "components": { "parameters": { "queryExample": { "in": "query", "name": "example", "schema": { "type": "object" } } }, @@ -247,7 +247,7 @@ "properties": { "chain": { "description": "Blockchain to add wallet to", - "enum": ["ethereum", "solana"], + "enum": ["ethereum", "solana", "cosmos"], "type": "string", "example": "solana" }, @@ -297,7 +297,7 @@ "properties": { "chain": { "description": "Blockchain for hardware wallet", - "enum": ["ethereum", "solana"], + "enum": ["ethereum", "solana", "cosmos"], "default": "solana", "type": "string", "example": "solana" @@ -351,7 +351,7 @@ "properties": { "chain": { "description": "Blockchain to remove wallet from", - "enum": ["ethereum", "solana"], + "enum": ["ethereum", "solana", "cosmos"], "type": "string", "example": "solana" }, @@ -393,7 +393,7 @@ "properties": { "chain": { "description": "Blockchain to set default wallet for", - "enum": ["ethereum", "solana"], + "enum": ["ethereum", "solana", "cosmos"], "type": "string", "example": "solana" }, @@ -407,6 +407,9 @@ }, "example2": { "value": { "chain": "solana", "address": "7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi" } + }, + "example3": { + "value": { "chain": "cosmos", "address": "osmo0000000000000000000000000000000000000000" } } } } @@ -433,6 +436,269 @@ } } }, + "/tokens/{symbolOrAddress}": { + "get": { + "tags": ["/tokens"], + "description": "Get a specific token by symbol or address", + "parameters": [ + { + "schema": { "type": "string" }, + "examples": { + "ethereum": { "value": "ethereum" }, + "solana": { "value": "solana" }, + "cosmos": { "value": "cosmos" } + }, + "in": "query", + "name": "chain", + "required": true, + "description": "Blockchain network (e.g., ethereum, solana)" + }, + { + "schema": { "type": "string" }, + "examples": { + "mainnet": { "value": "mainnet" }, + "mainnet-beta": { "value": "mainnet-beta" }, + "devnet": { "value": "devnet" }, + "testnet": { "value": "testnet" } + }, + "in": "query", + "name": "network", + "required": true, + "description": "Network name (e.g., mainnet, mainnet-beta)" + }, + { + "schema": { "type": "string" }, + "in": "path", + "name": "symbolOrAddress", + "required": true, + "description": "Token symbol or address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "token": { + "type": "object", + "properties": { + "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, + "name": { + "description": "The full name of the token", + "type": "string", + "example": "USD Coin" + }, + "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, + "address": { + "description": "The token contract address", + "type": "string", + "example": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "decimals": { + "description": "The number of decimals the token uses", + "minimum": 0, + "maximum": 255, + "type": "number", + "example": 6 + }, + "geckoData": { + "type": "object", + "properties": { + "coingeckoCoinId": { + "description": "CoinGecko coin ID if available", + "anyOf": [{ "type": "string" }, { "type": "null" }] + }, + "imageUrl": { "description": "Token image URL", "type": "string" }, + "priceUsd": { "description": "Current price in USD", "type": "string" }, + "volumeUsd24h": { "description": "24h trading volume in USD", "type": "string" }, + "marketCapUsd": { "description": "Market capitalization in USD", "type": "string" }, + "fdvUsd": { "description": "Fully diluted valuation in USD", "type": "string" }, + "totalSupply": { + "description": "Normalized total supply (human-readable)", + "type": "string" + }, + "topPools": { + "description": "Array of top pool addresses", + "type": "array", + "items": { "type": "string" } + }, + "timestamp": { + "description": "Unix timestamp (ms) when data was fetched", + "type": "number" + } + }, + "required": [ + "coingeckoCoinId", + "imageUrl", + "priceUsd", + "volumeUsd24h", + "marketCapUsd", + "fdvUsd", + "totalSupply", + "topPools", + "timestamp" + ] + } + }, + "required": ["name", "symbol", "address", "decimals"] + }, + "chain": { "type": "string" }, + "network": { "type": "string" } + }, + "required": ["token", "chain", "network"] + } + } + } + } + } + } + }, + "/tokens/find/{address}": { + "get": { + "tags": ["/tokens"], + "description": "Get token information with market data from GeckoTerminal by address", + "parameters": [ + { + "schema": { "type": "string" }, + "examples": { + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "ethereum-base": { "value": "ethereum-base" }, + "ethereum-polygon": { "value": "ethereum-polygon" } + }, + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, + { + "schema": { "type": "string" }, + "examples": { + "So11111111111111111111111111111111111111112": { "value": "So11111111111111111111111111111111111111112" }, + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } + }, + "in": "path", + "name": "address", + "required": true, + "description": "Token contract address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, + "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, + "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, + "address": { + "description": "The token contract address", + "type": "string", + "example": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "decimals": { + "description": "The number of decimals the token uses", + "minimum": 0, + "maximum": 255, + "type": "number", + "example": 6 + }, + "geckoData": { + "type": "object", + "allOf": [ + { + "type": "object", + "properties": { + "coingeckoCoinId": { + "description": "CoinGecko coin ID if available", + "anyOf": [{ "type": "string" }, { "type": "null" }] + }, + "imageUrl": { "description": "Token image URL", "type": "string" }, + "priceUsd": { "description": "Current price in USD", "type": "string" }, + "volumeUsd24h": { "description": "24h trading volume in USD", "type": "string" }, + "marketCapUsd": { "description": "Market capitalization in USD", "type": "string" }, + "fdvUsd": { "description": "Fully diluted valuation in USD", "type": "string" }, + "totalSupply": { + "description": "Normalized total supply (human-readable)", + "type": "string" + }, + "topPools": { + "description": "Array of top pool addresses", + "type": "array", + "items": { "type": "string" } + }, + "timestamp": { + "description": "Unix timestamp (ms) when data was fetched", + "type": "number" + } + }, + "required": [ + "coingeckoCoinId", + "imageUrl", + "priceUsd", + "volumeUsd24h", + "marketCapUsd", + "fdvUsd", + "totalSupply", + "topPools", + "timestamp" + ] + }, + { + "type": "object", + "properties": { + "coingeckoCoinId": { + "description": "CoinGecko coin ID if available", + "anyOf": [{ "type": "string" }, { "type": "null" }] + }, + "imageUrl": { "description": "Token image URL", "type": "string" }, + "priceUsd": { "description": "Current price in USD", "type": "string" }, + "volumeUsd24h": { "description": "24h trading volume in USD", "type": "string" }, + "marketCapUsd": { "description": "Market capitalization in USD", "type": "string" }, + "fdvUsd": { "description": "Fully diluted valuation in USD", "type": "string" }, + "totalSupply": { + "description": "Normalized total supply (human-readable)", + "type": "string" + }, + "topPools": { + "description": "Array of top pool addresses", + "type": "array", + "items": { "type": "string" } + }, + "timestamp": { + "description": "Unix timestamp (ms) when data was fetched", + "type": "number" + } + }, + "required": [ + "coingeckoCoinId", + "imageUrl", + "priceUsd", + "volumeUsd24h", + "marketCapUsd", + "fdvUsd", + "totalSupply", + "topPools", + "timestamp" + ] + } + ] + } + }, + "required": ["name", "symbol", "address", "decimals"] + } + } + } + } + } + } + }, "/tokens/": { "get": { "tags": ["/tokens"], @@ -440,7 +706,11 @@ "parameters": [ { "schema": { "type": "string" }, - "examples": { "ethereum": { "value": "ethereum" }, "solana": { "value": "solana" } }, + "examples": { + "ethereum": { "value": "ethereum" }, + "solana": { "value": "solana" }, + "cosmos": { "value": "cosmos" } + }, "in": "query", "name": "chain", "required": false, @@ -451,7 +721,8 @@ "examples": { "mainnet": { "value": "mainnet" }, "mainnet-beta": { "value": "mainnet-beta" }, - "devnet": { "value": "devnet" } + "devnet": { "value": "devnet" }, + "testnet": { "value": "testnet" } }, "in": "query", "name": "network", @@ -480,6 +751,7 @@ "items": { "type": "object", "properties": { + "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, "name": { "description": "The full name of the token", "type": "string", @@ -497,6 +769,44 @@ "maximum": 255, "type": "number", "example": 6 + }, + "geckoData": { + "type": "object", + "properties": { + "coingeckoCoinId": { + "description": "CoinGecko coin ID if available", + "anyOf": [{ "type": "string" }, { "type": "null" }] + }, + "imageUrl": { "description": "Token image URL", "type": "string" }, + "priceUsd": { "description": "Current price in USD", "type": "string" }, + "volumeUsd24h": { "description": "24h trading volume in USD", "type": "string" }, + "marketCapUsd": { "description": "Market capitalization in USD", "type": "string" }, + "fdvUsd": { "description": "Fully diluted valuation in USD", "type": "string" }, + "totalSupply": { + "description": "Normalized total supply (human-readable)", + "type": "string" + }, + "topPools": { + "description": "Array of top pool addresses", + "type": "array", + "items": { "type": "string" } + }, + "timestamp": { + "description": "Unix timestamp (ms) when data was fetched", + "type": "number" + } + }, + "required": [ + "coingeckoCoinId", + "imageUrl", + "priceUsd", + "volumeUsd24h", + "marketCapUsd", + "fdvUsd", + "totalSupply", + "topPools", + "timestamp" + ] } }, "required": ["name", "symbol", "address", "decimals"] @@ -532,6 +842,7 @@ "token": { "type": "object", "properties": { + "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, "address": { @@ -545,6 +856,41 @@ "maximum": 255, "type": "number", "example": 6 + }, + "geckoData": { + "type": "object", + "properties": { + "coingeckoCoinId": { + "description": "CoinGecko coin ID if available", + "anyOf": [{ "type": "string" }, { "type": "null" }] + }, + "imageUrl": { "description": "Token image URL", "type": "string" }, + "priceUsd": { "description": "Current price in USD", "type": "string" }, + "volumeUsd24h": { "description": "24h trading volume in USD", "type": "string" }, + "marketCapUsd": { "description": "Market capitalization in USD", "type": "string" }, + "fdvUsd": { "description": "Fully diluted valuation in USD", "type": "string" }, + "totalSupply": { + "description": "Normalized total supply (human-readable)", + "type": "string" + }, + "topPools": { + "description": "Array of top pool addresses", + "type": "array", + "items": { "type": "string" } + }, + "timestamp": { "description": "Unix timestamp (ms) when data was fetched", "type": "number" } + }, + "required": [ + "coingeckoCoinId", + "imageUrl", + "priceUsd", + "volumeUsd24h", + "marketCapUsd", + "fdvUsd", + "totalSupply", + "topPools", + "timestamp" + ] } }, "required": ["name", "symbol", "address", "decimals"] @@ -579,37 +925,34 @@ } } }, - "/tokens/{symbolOrAddress}": { - "get": { + "/tokens/save/{address}": { + "post": { "tags": ["/tokens"], - "description": "Get a specific token by symbol or address", + "description": "Find token from GeckoTerminal and save it to the token list", "parameters": [ - { - "schema": { "type": "string" }, - "examples": { "ethereum": { "value": "ethereum" }, "solana": { "value": "solana" } }, - "in": "query", - "name": "chain", - "required": true, - "description": "Blockchain network (e.g., ethereum, solana)" - }, { "schema": { "type": "string" }, "examples": { - "mainnet": { "value": "mainnet" }, - "mainnet-beta": { "value": "mainnet-beta" }, - "devnet": { "value": "devnet" } + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "ethereum-base": { "value": "ethereum-base" }, + "ethereum-polygon": { "value": "ethereum-polygon" } }, "in": "query", - "name": "network", + "name": "chainNetwork", "required": true, - "description": "Network name (e.g., mainnet, mainnet-beta)" + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { "schema": { "type": "string" }, + "examples": { + "So11111111111111111111111111111111111111112": { "value": "So11111111111111111111111111111111111111112" }, + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } + }, "in": "path", - "name": "symbolOrAddress", + "name": "address", "required": true, - "description": "Token symbol or address" + "description": "Token contract address" } ], "responses": { @@ -620,9 +963,11 @@ "schema": { "type": "object", "properties": { + "message": { "type": "string" }, "token": { "type": "object", "properties": { + "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, "name": { "description": "The full name of the token", "type": "string", @@ -640,14 +985,50 @@ "maximum": 255, "type": "number", "example": 6 + }, + "geckoData": { + "type": "object", + "properties": { + "coingeckoCoinId": { + "description": "CoinGecko coin ID if available", + "anyOf": [{ "type": "string" }, { "type": "null" }] + }, + "imageUrl": { "description": "Token image URL", "type": "string" }, + "priceUsd": { "description": "Current price in USD", "type": "string" }, + "volumeUsd24h": { "description": "24h trading volume in USD", "type": "string" }, + "marketCapUsd": { "description": "Market capitalization in USD", "type": "string" }, + "fdvUsd": { "description": "Fully diluted valuation in USD", "type": "string" }, + "totalSupply": { + "description": "Normalized total supply (human-readable)", + "type": "string" + }, + "topPools": { + "description": "Array of top pool addresses", + "type": "array", + "items": { "type": "string" } + }, + "timestamp": { + "description": "Unix timestamp (ms) when data was fetched", + "type": "number" + } + }, + "required": [ + "coingeckoCoinId", + "imageUrl", + "priceUsd", + "volumeUsd24h", + "marketCapUsd", + "fdvUsd", + "totalSupply", + "topPools", + "timestamp" + ] } }, "required": ["name", "symbol", "address", "decimals"] - }, - "chain": { "type": "string" }, - "network": { "type": "string" } + } }, - "required": ["token", "chain", "network"] + "required": ["message", "token"] } } } @@ -662,7 +1043,11 @@ "parameters": [ { "schema": { "type": "string" }, - "examples": { "ethereum": { "value": "ethereum" }, "solana": { "value": "solana" } }, + "examples": { + "ethereum": { "value": "ethereum" }, + "solana": { "value": "solana" }, + "cosmos": { "value": "cosmos" } + }, "in": "query", "name": "chain", "required": true, @@ -673,7 +1058,8 @@ "examples": { "mainnet": { "value": "mainnet" }, "mainnet-beta": { "value": "mainnet-beta" }, - "devnet": { "value": "devnet" } + "devnet": { "value": "devnet" }, + "testnet": { "value": "testnet" } }, "in": "query", "name": "network", @@ -711,10 +1097,10 @@ } } }, - "/pools/": { + "/pools/{tradingPair}": { "get": { "tags": ["/pools"], - "description": "List all pools for a connector, optionally filtered by network, type, or search term", + "description": "Get a specific pool by trading pair", "parameters": [ { "schema": { "type": "string" }, @@ -729,35 +1115,28 @@ "description": "Connector (raydium, meteora, uniswap)" }, { - "schema": { "type": "string" }, - "examples": { - "mainnet": { "value": "mainnet" }, - "mainnet-beta": { "value": "mainnet-beta" }, - "base": { "value": "base" } - }, + "schema": { "default": "mainnet-beta", "type": "string" }, + "examples": { "mainnet-beta": { "value": "mainnet-beta" }, "mainnet": { "value": "mainnet" } }, "in": "query", "name": "network", - "required": false, - "description": "Optional: filter by network (mainnet, mainnet-beta, etc)" + "required": true, + "description": "Network name (mainnet, mainnet-beta, etc)" }, { - "schema": { - "anyOf": [ - { "type": "string", "enum": ["amm"] }, - { "type": "string", "enum": ["clmm"] } - ] - }, + "schema": { "enum": ["amm", "clmm"], "type": "string" }, + "examples": { "amm": { "value": "amm" }, "clmm": { "value": "clmm" } }, "in": "query", "name": "type", - "required": false, - "description": "Optional: filter by pool type" + "required": true, + "description": "Pool type" }, { "schema": { "type": "string" }, - "in": "query", - "name": "search", - "required": false, - "description": "Optional: search by token symbol or address" + "examples": { "SOL-USDC": { "value": "SOL-USDC" }, "ETH-USDC": { "value": "ETH-USDC" } }, + "in": "path", + "name": "tradingPair", + "required": true, + "description": "Trading pair (e.g., SOL-USDC, ETH-USDC)" } ], "responses": { @@ -766,65 +1145,77 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "anyOf": [ - { "type": "string", "enum": ["amm"] }, - { "type": "string", "enum": ["clmm"] } - ] - }, - "network": { "type": "string" }, - "baseSymbol": { "type": "string" }, - "quoteSymbol": { "type": "string" }, - "address": { "type": "string" } + "type": "object", + "properties": { + "type": { + "description": "Pool type", + "enum": ["clmm", "amm"], + "type": "string", + "example": "clmm" }, - "required": ["type", "network", "baseSymbol", "quoteSymbol", "address"] - } + "network": { "type": "string" }, + "baseSymbol": { "type": "string" }, + "quoteSymbol": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "address": { "type": "string" } + }, + "required": [ + "type", + "network", + "baseSymbol", + "quoteSymbol", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "address" + ] } } } + }, + "404": { + "description": "Default Response", + "content": { + "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } + } } } - }, - "post": { + } + }, + "/pools/find/{address}": { + "get": { "tags": ["/pools"], - "description": "Add a new pool", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "connector": { - "description": "Connector (raydium, meteora, uniswap)", - "type": "string", - "example": "raydium" - }, - "type": { - "description": "Pool type", - "anyOf": [ - { "type": "string", "enum": ["amm"] }, - { "type": "string", "enum": ["clmm"] } - ] - }, - "network": { - "description": "Network name (mainnet, mainnet-beta, etc)", - "type": "string", - "example": "mainnet" - }, - "baseSymbol": { "description": "Base token symbol", "type": "string", "example": "ETH" }, - "quoteSymbol": { "description": "Quote token symbol", "type": "string", "example": "USDC" }, - "address": { "description": "Pool contract address", "type": "string" } - }, - "required": ["connector", "type", "network", "baseSymbol", "quoteSymbol", "address"] - } - } + "description": "Get detailed pool information by address from GeckoTerminal", + "parameters": [ + { + "schema": { "type": "string" }, + "examples": { + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "ethereum-base": { "value": "ethereum-base" }, + "ethereum-polygon": { "value": "ethereum-polygon" } + }, + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, - "required": true - }, + { + "schema": { "type": "string" }, + "examples": { + "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2": { + "value": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" + }, + "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640": { "value": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" } + }, + "in": "path", + "name": "address", + "required": true, + "description": "Pool contract address" + } + ], "responses": { "200": { "description": "Default Response", @@ -832,66 +1223,132 @@ "application/json": { "schema": { "type": "object", - "properties": { "message": { "type": "string" } }, - "required": ["message"] + "properties": { + "type": { + "description": "Pool type", + "enum": ["clmm", "amm"], + "type": "string", + "example": "clmm" + }, + "network": { "type": "string" }, + "baseSymbol": { "type": "string" }, + "quoteSymbol": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "address": { "type": "string" }, + "geckoData": { + "type": "object", + "properties": { + "volumeUsd24h": { "description": "24-hour trading volume in USD", "type": "string" }, + "liquidityUsd": { "description": "Total liquidity in USD", "type": "string" }, + "priceNative": { "description": "Base token price in quote token", "type": "string" }, + "priceUsd": { "description": "Base token price in USD", "type": "string" }, + "buys24h": { "description": "Number of buy transactions in 24h", "type": "number" }, + "sells24h": { "description": "Number of sell transactions in 24h", "type": "number" }, + "apr": { "description": "Annual percentage rate", "type": "number" }, + "timestamp": { "description": "Unix timestamp (ms) when data was fetched", "type": "number" } + }, + "required": [ + "volumeUsd24h", + "liquidityUsd", + "priceNative", + "priceUsd", + "buys24h", + "sells24h", + "timestamp" + ] + } + }, + "required": [ + "type", + "network", + "baseSymbol", + "quoteSymbol", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "address" + ] } } } - }, - "400": { - "description": "Default Response", - "content": { - "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } - } } } } }, - "/pools/{tradingPair}": { + "/pools/find": { "get": { "tags": ["/pools"], - "description": "Get a specific pool by trading pair", + "description": "Find pools for a token pair from GeckoTerminal", "parameters": [ + { + "schema": { "type": "string" }, + "examples": { + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "ethereum-base": { "value": "ethereum-base" }, + "ethereum-polygon": { "value": "ethereum-polygon" } + }, + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, { "schema": { "type": "string" }, "examples": { "raydium": { "value": "raydium" }, "meteora": { "value": "meteora" }, - "uniswap": { "value": "uniswap" } + "uniswap": { "value": "uniswap" }, + "pancakeswap": { "value": "pancakeswap" }, + "pancakeswap-sol": { "value": "pancakeswap-sol" } }, "in": "query", "name": "connector", - "required": true, - "description": "Connector (raydium, meteora, uniswap)" + "required": false, + "description": "Filter by connector name (e.g., raydium, meteora, uniswap, pancakeswap, pancakeswap-sol)" }, { - "schema": { "type": "string" }, - "examples": { "mainnet": { "value": "mainnet" }, "mainnet-beta": { "value": "mainnet-beta" } }, + "schema": { "enum": ["clmm", "amm"], "default": "clmm", "type": "string" }, + "examples": { "clmm": { "value": "clmm" }, "amm": { "value": "amm" } }, "in": "query", - "name": "network", - "required": true, - "description": "Network name (mainnet, mainnet-beta, etc)" + "name": "type", + "required": false, + "description": "Filter by pool type: clmm (v3-style concentrated liquidity) or amm (v2-style)" }, { - "schema": { - "anyOf": [ - { "type": "string", "enum": ["amm"] }, - { "type": "string", "enum": ["clmm"] } - ] + "schema": { "type": "string" }, + "examples": { + "SOL": { "value": "SOL" }, + "So11111111111111111111111111111111111111112": { "value": "So11111111111111111111111111111111111111112" }, + "USDC": { "value": "USDC" } }, - "examples": { "amm": { "value": "amm" }, "clmm": { "value": "clmm" } }, "in": "query", - "name": "type", - "required": true, - "description": "Pool type" + "name": "tokenA", + "required": false, + "description": "First token symbol or contract address (optional - for filtering by token pair)" }, { "schema": { "type": "string" }, - "examples": { "ETH-USDC": { "value": "ETH-USDC" }, "SOL-USDC": { "value": "SOL-USDC" } }, - "in": "path", - "name": "tradingPair", - "required": true, - "description": "Trading pair (e.g., ETH-USDC, SOL-USDC)" + "examples": { + "USDC": { "value": "USDC" }, + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": { + "value": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + }, + "SOL": { "value": "SOL" } + }, + "in": "query", + "name": "tokenB", + "required": false, + "description": "Second token symbol or contract address (optional - for filtering by token pair)" + }, + { + "schema": { "minimum": 1, "maximum": 10, "default": 10, "type": "number" }, + "in": "query", + "name": "pages", + "required": false, + "description": "Number of pages to fetch from GeckoTerminal (1-10, default: 10)" } ], "responses": { @@ -900,37 +1357,68 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "type": { - "anyOf": [ - { "type": "string", "enum": ["amm"] }, - { "type": "string", "enum": ["clmm"] } - ] + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "description": "Pool type", + "enum": ["clmm", "amm"], + "type": "string", + "example": "clmm" + }, + "network": { "type": "string" }, + "baseSymbol": { "type": "string" }, + "quoteSymbol": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "address": { "type": "string" }, + "geckoData": { + "type": "object", + "properties": { + "volumeUsd24h": { "description": "24-hour trading volume in USD", "type": "string" }, + "liquidityUsd": { "description": "Total liquidity in USD", "type": "string" }, + "priceNative": { "description": "Base token price in quote token", "type": "string" }, + "priceUsd": { "description": "Base token price in USD", "type": "string" }, + "buys24h": { "description": "Number of buy transactions in 24h", "type": "number" }, + "sells24h": { "description": "Number of sell transactions in 24h", "type": "number" }, + "apr": { "description": "Annual percentage rate", "type": "number" }, + "timestamp": { "description": "Unix timestamp (ms) when data was fetched", "type": "number" } + }, + "required": [ + "volumeUsd24h", + "liquidityUsd", + "priceNative", + "priceUsd", + "buys24h", + "sells24h", + "timestamp" + ] + } }, - "network": { "type": "string" }, - "baseSymbol": { "type": "string" }, - "quoteSymbol": { "type": "string" }, - "address": { "type": "string" } - }, - "required": ["type", "network", "baseSymbol", "quoteSymbol", "address"] + "required": [ + "type", + "network", + "baseSymbol", + "quoteSymbol", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "address" + ] + } } } } - }, - "404": { - "description": "Default Response", - "content": { - "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } - } } } } }, - "/pools/{address}": { - "delete": { + "/pools/": { + "get": { "tags": ["/pools"], - "description": "Remove a pool by address", + "description": "List all pools for a connector, optionally filtered by network, type, or search term", "parameters": [ { "schema": { "type": "string" }, @@ -946,65 +1434,30 @@ }, { "schema": { "type": "string" }, - "examples": { "mainnet": { "value": "mainnet" }, "mainnet-beta": { "value": "mainnet-beta" } }, + "examples": { + "mainnet": { "value": "mainnet" }, + "mainnet-beta": { "value": "mainnet-beta" }, + "base": { "value": "base" } + }, "in": "query", "name": "network", - "required": true, - "description": "Network name (mainnet, mainnet-beta, etc)" + "required": false, + "description": "Optional: filter by network (mainnet, mainnet-beta, etc)" }, { - "schema": { - "anyOf": [ - { "type": "string", "enum": ["amm"] }, - { "type": "string", "enum": ["clmm"] } - ] - }, + "schema": { "enum": ["clmm", "amm"], "type": "string" }, + "examples": { "clmm": { "value": "clmm" }, "amm": { "value": "amm" } }, "in": "query", "name": "type", - "required": true, - "description": "Pool type" + "required": false, + "description": "Optional: filter by pool type" }, { "schema": { "type": "string" }, - "in": "path", - "name": "address", - "required": true, - "description": "Pool contract address to remove" - } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { "message": { "type": "string" } }, - "required": ["message"] - } - } - } - }, - "404": { - "description": "Default Response", - "content": { - "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } - } - } - } - } - }, - "/chains/solana/status": { - "get": { - "tags": ["/chain/solana"], - "description": "Get Solana network status", - "parameters": [ - { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", - "name": "network", + "name": "search", "required": false, - "description": "The Solana network to use" + "description": "Optional: search by token symbol or address" } ], "responses": { @@ -1013,123 +1466,152 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "chain": { "type": "string" }, - "network": { "type": "string" }, - "rpcUrl": { "type": "string" }, - "currentBlockNumber": { "type": "number" }, - "nativeCurrency": { "type": "string" } - }, - "required": ["chain", "network", "rpcUrl", "currentBlockNumber", "nativeCurrency"] + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "description": "Pool type", + "enum": ["clmm", "amm"], + "type": "string", + "example": "clmm" + }, + "network": { "type": "string" }, + "baseSymbol": { "type": "string" }, + "quoteSymbol": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "address": { "type": "string" } + }, + "required": [ + "type", + "network", + "baseSymbol", + "quoteSymbol", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "address" + ] + } } } } } } - } - }, - "/chains/solana/balances": { + }, "post": { - "tags": ["/chain/solana"], - "description": "Get token balances for a Solana address. If no tokens specified or empty array provided, returns non-zero balances for tokens from the token list that are found in the wallet (includes SOL even if zero). If specific tokens are requested, returns those exact tokens with their balances, including zeros.", + "tags": ["/pools"], + "description": "Add a new pool", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { + "connector": { + "description": "Connector (raydium, meteora, uniswap)", + "type": "string", + "example": "raydium" + }, + "type": { "description": "Pool type", "enum": ["clmm", "amm"], "type": "string", "example": "clmm" }, "network": { - "description": "The Solana network to use", + "description": "Network name (mainnet, mainnet-beta, etc)", "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" + "type": "string", + "example": "mainnet-beta" }, - "address": { - "description": "Solana wallet address", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "type": "string" + "address": { "description": "Pool contract address", "type": "string" }, + "baseSymbol": { "description": "Base token symbol", "type": "string", "example": "SOL" }, + "quoteSymbol": { "description": "Quote token symbol", "type": "string", "example": "USDC" }, + "baseTokenAddress": { + "description": "Base token contract address", + "type": "string", + "example": "So11111111111111111111111111111111111111112" }, - "tokens": { - "description": "A list of token symbols (SOL, USDC, BONK) or token mint addresses. Both formats are accepted and will be automatically detected. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances (with the exception of SOL).", - "type": "array", - "items": { "type": "string" }, - "example": ["SOL", "USDC", "BONK"] + "quoteTokenAddress": { + "description": "Quote token contract address", + "type": "string", + "example": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }, - "fetchAll": { - "description": "Whether to fetch all tokens in wallet, not just those in token list", - "default": false, - "type": "boolean" + "feePct": { + "description": "Pool fee percentage (optional - fetched from pool-info if not provided)", + "minimum": 0, + "maximum": 100, + "type": "number", + "example": 0.25 } - } + }, + "required": [ + "connector", + "type", + "network", + "address", + "baseSymbol", + "quoteSymbol", + "baseTokenAddress", + "quoteTokenAddress" + ] } } - } + }, + "required": true }, "responses": { "200": { - "description": "Token balances for the specified address", + "description": "Default Response", "content": { "application/json": { "schema": { "type": "object", - "properties": { "balances": { "type": "object", "additionalProperties": { "type": "number" } } }, - "required": ["balances"], - "description": "Token balances for the specified address" - }, - "examples": { - "example1": { "value": { "balances": { "SOL": 1.5, "USDC": 100, "BONK": 50000 } } }, - "example2": { - "value": { - "balances": { "SOL": 1.5, "USDC": 100, "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": 25 } - } - } + "properties": { "message": { "type": "string" } }, + "required": ["message"] } } } + }, + "400": { + "description": "Default Response", + "content": { + "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } + } } } } }, - "/chains/solana/poll": { + "/pools/save/{address}": { "post": { - "tags": ["/chain/solana"], - "description": "Poll for the status of a Solana transaction", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "network": { - "description": "The Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "signature": { - "description": "Transaction signature to poll", - "type": "string", - "example": "55ukR6VCt1sQFMC8Nyeo51R1SMaTzUC7jikmkEJ2jjkQNdqBxXHraH7vaoaNmf8rX4Y55EXAj8XXoyzvvsrQqWZa" - }, - "tokens": { - "description": "Tokens to track balance changes for", - "type": "array", - "items": { "type": "string" }, - "example": ["SOL", "USDC", "BONK"] - }, - "walletAddress": { - "description": "Wallet address to track balance changes for", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "type": "string" - } - }, - "required": ["signature"] - } - } + "tags": ["/pools"], + "description": "Find pool from GeckoTerminal and save it to the pool list", + "parameters": [ + { + "schema": { "type": "string" }, + "examples": { + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "ethereum-base": { "value": "ethereum-base" }, + "ethereum-polygon": { "value": "ethereum-polygon" } + }, + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, - "required": true - }, + { + "schema": { "type": "string" }, + "examples": { + "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2": { + "value": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" + }, + "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640": { "value": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" } + }, + "in": "path", + "name": "address", + "required": true, + "description": "Pool contract address" + } + ], "responses": { "200": { "description": "Default Response", @@ -1138,20 +1620,62 @@ "schema": { "type": "object", "properties": { - "currentBlock": { "type": "number" }, - "signature": { "type": "string" }, - "txBlock": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, - "txStatus": { "type": "number" }, - "fee": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, - "tokenBalanceChanges": { - "description": "Dictionary of token balance changes keyed by token input value (symbol or address)", + "message": { "type": "string" }, + "pool": { "type": "object", - "additionalProperties": { "type": "number" } - }, - "txData": { "anyOf": [{ "type": "object", "additionalProperties": {} }, { "type": "null" }] }, - "error": { "type": "string" } + "properties": { + "type": { + "description": "Pool type", + "enum": ["clmm", "amm"], + "type": "string", + "example": "clmm" + }, + "network": { "type": "string" }, + "baseSymbol": { "type": "string" }, + "quoteSymbol": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "address": { "type": "string" }, + "geckoData": { + "type": "object", + "properties": { + "volumeUsd24h": { "description": "24-hour trading volume in USD", "type": "string" }, + "liquidityUsd": { "description": "Total liquidity in USD", "type": "string" }, + "priceNative": { "description": "Base token price in quote token", "type": "string" }, + "priceUsd": { "description": "Base token price in USD", "type": "string" }, + "buys24h": { "description": "Number of buy transactions in 24h", "type": "number" }, + "sells24h": { "description": "Number of sell transactions in 24h", "type": "number" }, + "apr": { "description": "Annual percentage rate", "type": "number" }, + "timestamp": { + "description": "Unix timestamp (ms) when data was fetched", + "type": "number" + } + }, + "required": [ + "volumeUsd24h", + "liquidityUsd", + "priceNative", + "priceUsd", + "buys24h", + "sells24h", + "timestamp" + ] + } + }, + "required": [ + "type", + "network", + "baseSymbol", + "quoteSymbol", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "address" + ] + } }, - "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "txData"] + "required": ["message", "pool"] } } } @@ -1159,27 +1683,51 @@ } } }, - "/chains/solana/estimate-gas": { - "post": { - "tags": ["/chain/solana"], - "description": "Estimate gas prices for Solana transactions", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "network": { - "description": "The Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - } - } - } - } + "/pools/{address}": { + "delete": { + "tags": ["/pools"], + "description": "Remove a pool by address", + "parameters": [ + { + "schema": { "type": "string" }, + "examples": { + "raydium": { "value": "raydium" }, + "meteora": { "value": "meteora" }, + "uniswap": { "value": "uniswap" } + }, + "in": "query", + "name": "connector", + "required": true, + "description": "Connector (raydium, meteora, uniswap)" + }, + { + "schema": { "type": "string" }, + "examples": { "mainnet": { "value": "mainnet" }, "mainnet-beta": { "value": "mainnet-beta" } }, + "in": "query", + "name": "network", + "required": true, + "description": "Network name (mainnet, mainnet-beta, etc)" + }, + { + "schema": { + "anyOf": [ + { "type": "string", "enum": ["amm"] }, + { "type": "string", "enum": ["clmm"] } + ] + }, + "in": "query", + "name": "type", + "required": true, + "description": "Pool type" + }, + { + "schema": { "type": "string" }, + "in": "path", + "name": "address", + "required": true, + "description": "Pool contract address to remove" } - }, + ], "responses": { "200": { "description": "Default Response", @@ -1187,34 +1735,74 @@ "application/json": { "schema": { "type": "object", - "properties": { - "feePerComputeUnit": { "type": "number" }, - "denomination": { "type": "string" }, - "timestamp": { "type": "number" } - }, - "required": ["feePerComputeUnit", "denomination", "timestamp"] + "properties": { "message": { "type": "string" } }, + "required": ["message"] } } } + }, + "404": { + "description": "Default Response", + "content": { + "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } + } } } } }, - "/chains/ethereum/status": { + "/trading/swap/quote": { "get": { - "tags": ["/chain/ethereum"], - "description": "Get Ethereum chain status", + "tags": ["/trading/swap"], + "description": "Get a swap quote for any supported chain", "parameters": [ { - "schema": { - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon", "sepolia"], - "type": "string" - }, + "schema": { "default": "solana-mainnet-beta", "type": "string" }, "in": "query", - "name": "network", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet, ethereum-polygon)" + }, + { + "schema": { "default": "jupiter/router", "type": "string" }, + "in": "query", + "name": "connector", "required": false, - "description": "The Ethereum network to use" + "description": "Connector to use in format: connector/type (e.g., jupiter/router, raydium/amm, uniswap/clmm). If not provided, uses network's configured swapProvider" + }, + { + "schema": { "default": "SOL", "type": "string" }, + "in": "query", + "name": "baseToken", + "required": true, + "description": "Symbol or address of the base token" + }, + { + "schema": { "default": "USDC", "type": "string" }, + "in": "query", + "name": "quoteToken", + "required": true, + "description": "Symbol or address of the quote token" + }, + { + "schema": { "default": 1, "type": "number" }, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount to swap" + }, + { + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Side of the swap" + }, + { + "schema": { "default": 1, "type": "number" }, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Slippage tolerance percentage (optional)" } ], "responses": { @@ -1225,13 +1813,31 @@ "schema": { "type": "object", "properties": { - "chain": { "type": "string" }, - "network": { "type": "string" }, - "rpcUrl": { "type": "string" }, - "currentBlockNumber": { "type": "number" }, - "nativeCurrency": { "type": "string" } + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "minAmountOut": { + "description": "Minimum amount of tokenOut that will be accepted", + "type": "number" + }, + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "poolAddress": { "description": "Pool address for AMM/CLMM swaps", "type": "string" }, + "routePath": { "description": "Route path for router-based swaps", "type": "string" }, + "slippagePct": { "description": "Slippage tolerance percentage", "type": "number" } }, - "required": ["chain", "network", "rpcUrl", "currentBlockNumber", "nativeCurrency"] + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "priceImpactPct", + "minAmountOut", + "maxAmountIn" + ] } } } @@ -1239,47 +1845,59 @@ } } }, - "/chains/ethereum/balances": { + "/trading/swap/execute": { "post": { - "tags": ["/chain/ethereum"], - "description": "Get Ethereum balances. If no tokens specified or empty array provided, returns native token (ETH) and only non-zero balances for tokens from the token list. If specific tokens are requested, returns those exact tokens with their balances, including zeros.", + "tags": ["/trading/swap"], + "description": "Execute a swap on any supported chain", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { - "description": "The Ethereum network to use", - "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" - ], + "walletAddress": { + "description": "Wallet address to execute swap from", + "default": "", "type": "string" }, - "address": { - "description": "Ethereum wallet address", - "default": "", + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet, ethereum-polygon)", + "default": "solana-mainnet-beta", "type": "string" }, - "tokens": { - "description": "A list of token symbols (ETH, USDC, WETH) or token addresses. Both formats are accepted and will be automatically detected. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances (with the exception of ETH).", - "type": "array", - "items": { "type": "string" }, - "example": ["ETH", "USDC", "WETH"] + "connector": { + "description": "Connector to use in format: connector/type (e.g., jupiter/router, raydium/amm, uniswap/clmm). If not provided, uses network's configured swapProvider", + "default": "jupiter/router", + "type": "string" + }, + "baseToken": { + "description": "Symbol or address of the base token", + "default": "SOL", + "type": "string" + }, + "quoteToken": { + "description": "Symbol or address of the quote token", + "default": "USDC", + "type": "string" + }, + "amount": { "description": "Amount to swap", "default": 1, "type": "number" }, + "side": { + "description": "Side of the swap", + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, + "slippagePct": { + "description": "Slippage tolerance percentage (optional)", + "default": 1, + "type": "number" } - } + }, + "required": ["walletAddress", "chainNetwork", "baseToken", "quoteToken", "amount", "side"] } } - } + }, + "required": true }, "responses": { "200": { @@ -1288,8 +1906,41 @@ "application/json": { "schema": { "type": "object", - "properties": { "balances": { "type": "object", "additionalProperties": { "type": "number" } } }, - "required": ["balances"] + "properties": { + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] } } } @@ -1297,44 +1948,109 @@ } } }, - "/chains/ethereum/poll": { - "post": { - "tags": ["/chain/ethereum"], - "description": "Poll Ethereum transaction status", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "network": { - "description": "The Ethereum network to use", - "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" - ], - "type": "string" + "/trading/clmm/pool-info": { + "get": { + "tags": ["/trading/clmm"], + "description": "Get CLMM pool information from any supported connector", + "parameters": [ + { + "schema": { + "enum": ["raydium", "meteora", "pancakeswap-sol", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string" + }, + "example": "meteora", + "in": "query", + "name": "connector", + "required": true, + "description": "CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap)" + }, + { + "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "example": "solana-mainnet-beta", + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, + { + "schema": { "type": "string" }, + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Pool contract address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" } }, - "signature": { - "description": "Transaction hash to poll", - "type": "string", - "example": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" - } - }, - "required": ["signature"] + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount", + "activeBinId" + ] + } } } + } + } + } + }, + "/trading/clmm/position-info": { + "get": { + "tags": ["/trading/clmm"], + "description": "Get CLMM position information from any supported connector", + "parameters": [ + { + "schema": { + "enum": ["raydium", "meteora", "pancakeswap-sol", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string" + }, + "example": "meteora", + "in": "query", + "name": "connector", + "required": true, + "description": "CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap)" }, - "required": true - }, + { + "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "example": "solana-mainnet-beta", + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, + { + "schema": { "type": "string" }, + "example": "", + "in": "query", + "name": "positionAddress", + "required": true, + "description": "Position address or NFT token ID" + } + ], "responses": { "200": { "description": "Default Response", @@ -1343,20 +2059,37 @@ "schema": { "type": "object", "properties": { - "currentBlock": { "type": "number" }, - "signature": { "type": "string" }, - "txBlock": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, - "txStatus": { "type": "number" }, - "fee": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, - "tokenBalanceChanges": { - "description": "Dictionary of token balance changes keyed by token input value (symbol or address)", - "type": "object", - "additionalProperties": { "type": "number" } - }, - "txData": { "anyOf": [{ "type": "object", "additionalProperties": {} }, { "type": "null" }] }, - "error": { "type": "string" } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } }, - "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "txData"] + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] } } } @@ -1364,55 +2097,4825 @@ } } }, - "/chains/ethereum/allowances": { - "post": { - "tags": ["/chain/ethereum"], - "description": "Get token allowances", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "network": { - "description": "The Ethereum network to use", - "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" + "/trading/clmm/positions-owned": { + "get": { + "tags": ["/trading/clmm"], + "description": "Get all CLMM positions owned by a wallet from any supported connector", + "parameters": [ + { + "schema": { + "enum": ["raydium", "meteora", "pancakeswap-sol", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string" + }, + "example": "meteora", + "in": "query", + "name": "connector", + "required": true, + "description": "CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap)" + }, + { + "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "example": "solana-mainnet-beta", + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, + { + "schema": { "default": "", "type": "string" }, + "in": "query", + "name": "walletAddress", + "required": false, + "description": "Wallet address (optional, uses default wallet if not provided)" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" ], + "title": "PositionInfo" + } + } + } + } + } + } + } + }, + "/trading/clmm/quote-position": { + "get": { + "tags": ["/trading/clmm"], + "description": "Quote amounts for a new CLMM position from any supported connector", + "parameters": [ + { + "schema": { + "enum": ["raydium", "meteora", "pancakeswap-sol", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string" + }, + "example": "meteora", + "in": "query", + "name": "connector", + "required": true, + "description": "CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap)" + }, + { + "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "example": "solana-mainnet-beta", + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, + { + "schema": { "type": "number" }, + "example": 150, + "in": "query", + "name": "lowerPrice", + "required": true, + "description": "Lower price bound for the position" + }, + { + "schema": { "type": "number" }, + "example": 250, + "in": "query", + "name": "upperPrice", + "required": true, + "description": "Upper price bound for the position" + }, + { + "schema": { "type": "string" }, + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Pool contract address" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "baseTokenAmount", + "required": false, + "description": "Amount of base token to deposit" + }, + { + "schema": { "type": "number" }, + "example": 2, + "in": "query", + "name": "quoteTokenAmount", + "required": false, + "description": "Amount of quote token to deposit" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "example": 1, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, + "liquidity": {} + }, + "required": [ + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" + ] + } + } + } + } + } + } + }, + "/trading/clmm/open": { + "post": { + "tags": ["/trading/clmm"], + "description": "Open a new CLMM position across supported connectors", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol)", + "default": "meteora", + "type": "string", + "example": "meteora" + }, + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", + "default": "solana-mainnet-beta", + "type": "string", + "example": "solana-mainnet-beta" + }, + "walletAddress": { + "description": "Wallet address", + "default": "", "type": "string" }, - "address": { - "description": "Ethereum wallet address", + "lowerPrice": { + "description": "Lower price bound for the position", + "type": "number", + "example": 150 + }, + "upperPrice": { + "description": "Upper price bound for the position", + "type": "number", + "example": 250 + }, + "poolAddress": { + "description": "Pool address", + "type": "string", + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3" + }, + "baseTokenAmount": { + "description": "Amount of base token to deposit", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { + "description": "Amount of quote token to deposit", + "type": "number", + "example": 2 + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 1, + "type": "number", + "example": 1 + } + }, + "required": ["connector", "chainNetwork", "walletAddress", "lowerPrice", "upperPrice", "poolAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": [ + "fee", + "positionAddress", + "positionRent", + "baseTokenAmountAdded", + "quoteTokenAmountAdded" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/trading/clmm/add": { + "post": { + "tags": ["/trading/clmm"], + "description": "Add liquidity to an existing CLMM position across supported connectors", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol)", + "default": "meteora", + "type": "string", + "example": "meteora" + }, + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", + "default": "solana-mainnet-beta", + "type": "string", + "example": "solana-mainnet-beta" + }, + "walletAddress": { + "description": "Wallet address", + "default": "", + "type": "string" + }, + "positionAddress": { + "description": "Position address", + "type": "string", + "example": "" + }, + "baseTokenAmount": { + "description": "Amount of base token to deposit", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { + "description": "Amount of quote token to deposit", + "type": "number", + "example": 2 + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 1, + "type": "number", + "example": 1 + } + }, + "required": [ + "connector", + "chainNetwork", + "walletAddress", + "positionAddress", + "baseTokenAmount", + "quoteTokenAmount" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" }, + "newPositionAddress": { "type": "string" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/trading/clmm/remove": { + "post": { + "tags": ["/trading/clmm"], + "description": "Remove liquidity from a CLMM position across supported connectors", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol)", + "default": "meteora", + "type": "string", + "example": "meteora" + }, + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", + "default": "solana-mainnet-beta", + "type": "string", + "example": "solana-mainnet-beta" + }, + "walletAddress": { + "description": "Wallet address", + "default": "", + "type": "string" + }, + "positionAddress": { + "description": "Position address", + "type": "string", + "example": "" + }, + "percentageToRemove": { + "minimum": 0, + "maximum": 100, + "description": "Percentage of liquidity to remove", + "default": 100, + "type": "number", + "example": 100 + } + }, + "required": ["connector", "chainNetwork", "walletAddress", "positionAddress", "percentageToRemove"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/trading/clmm/collect-fees": { + "post": { + "tags": ["/trading/clmm"], + "description": "Collect fees from a CLMM position across supported connectors", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol)", + "default": "meteora", + "type": "string", + "example": "meteora" + }, + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", + "default": "solana-mainnet-beta", + "type": "string", + "example": "solana-mainnet-beta" + }, + "walletAddress": { + "description": "Wallet address", + "default": "", + "type": "string" + }, + "positionAddress": { + "description": "Position address", + "type": "string", + "example": "" + } + }, + "required": ["connector", "chainNetwork", "walletAddress", "positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/trading/clmm/close": { + "post": { + "tags": ["/trading/clmm"], + "description": "Close a CLMM position across supported connectors", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol)", + "default": "meteora", + "type": "string", + "example": "meteora" + }, + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", + "default": "solana-mainnet-beta", + "type": "string", + "example": "solana-mainnet-beta" + }, + "walletAddress": { + "description": "Wallet address", + "default": "", + "type": "string" + }, + "positionAddress": { + "description": "Position address", + "type": "string", + "example": "" + } + }, + "required": ["connector", "chainNetwork", "walletAddress", "positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": [ + "fee", + "positionRentRefunded", + "baseTokenAmountRemoved", + "quoteTokenAmountRemoved", + "baseFeeAmountCollected", + "quoteFeeAmountCollected" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/chains/solana/status": { + "get": { + "tags": ["/chain/solana"], + "description": "Get Solana network status", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "The Solana network to use" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "chain": { "type": "string" }, + "network": { "type": "string" }, + "rpcUrl": { "type": "string" }, + "rpcProvider": { "type": "string" }, + "currentBlockNumber": { "type": "number" }, + "nativeCurrency": { "type": "string" }, + "swapProvider": { "type": "string" } + }, + "required": [ + "chain", + "network", + "rpcUrl", + "rpcProvider", + "currentBlockNumber", + "nativeCurrency", + "swapProvider" + ] + } + } + } + } + } + } + }, + "/chains/solana/estimate-gas": { + "get": { + "tags": ["/chain/solana"], + "description": "Estimate gas prices for Solana transactions", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "The Solana network to use" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "feePerComputeUnit": { "type": "number" }, + "denomination": { "type": "string" }, + "computeUnits": { "type": "number" }, + "feeAsset": { "type": "string" }, + "fee": { "type": "number" }, + "timestamp": { "type": "number" }, + "gasType": { "type": "string" }, + "maxFeePerGas": { "type": "number" }, + "maxPriorityFeePerGas": { "type": "number" } + }, + "required": ["feePerComputeUnit", "denomination", "computeUnits", "feeAsset", "fee", "timestamp"] + } + } + } + } + } + } + }, + "/chains/solana/balances": { + "post": { + "tags": ["/chain/solana"], + "description": "Get token balances for a Solana address. Only returns tokens in the network's token list. If no tokens specified or empty array provided, returns non-zero balances for tokens from the token list that are found in the wallet (includes SOL even if zero). If specific tokens are requested, returns those exact tokens with their balances, including zeros.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "address": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "tokens": { + "description": "A list of token symbols (SOL, USDC, BONK) from the network's token list. Only tokens in the token list will be returned. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances (with the exception of SOL).", + "type": "array", + "items": { "type": "string" }, + "example": ["SOL", "USDC", "BONK"] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Token balances for the specified address (only tokens in token list)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "balances": { "type": "object", "additionalProperties": { "type": "number" } } }, + "required": ["balances"], + "description": "Token balances for the specified address (only tokens in token list)" + }, + "example": { "balances": { "SOL": 1.5, "USDC": 100, "BONK": 50000 } } + } + } + } + } + } + }, + "/chains/solana/poll": { + "post": { + "tags": ["/chain/solana"], + "description": "Poll for the status of a Solana transaction", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "signature": { + "description": "Transaction signature to poll", + "type": "string", + "example": "55ukR6VCt1sQFMC8Nyeo51R1SMaTzUC7jikmkEJ2jjkQNdqBxXHraH7vaoaNmf8rX4Y55EXAj8XXoyzvvsrQqWZa" + }, + "tokens": { + "description": "Tokens to track balance changes for", + "type": "array", + "items": { "type": "string" }, + "example": ["SOL", "USDC", "BONK"] + }, + "walletAddress": { + "description": "Wallet address to track balance changes for", + "default": "", + "type": "string" + } + }, + "required": ["signature"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "currentBlock": { "type": "number" }, + "signature": { "type": "string" }, + "txBlock": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "txStatus": { "type": "number" }, + "fee": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "tokenBalanceChanges": { + "description": "Dictionary of token balance changes keyed by token input value (symbol or address)", + "type": "object", + "additionalProperties": { "type": "number" } + }, + "txData": { "anyOf": [{ "type": "object", "additionalProperties": {} }, { "type": "null" }] }, + "error": { "type": "string" } + }, + "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "txData"] + } + } + } + } + } + } + }, + "/chains/solana/wrap": { + "post": { + "tags": ["/chain/solana"], + "description": "Wrap SOL to WSOL (Wrapped SOL)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "address": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "amount": { + "description": "The amount of SOL to wrap (in SOL, not lamports)", + "type": "string", + "example": "1.0" + } + }, + "required": ["amount"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "string" }, + "amount": { "type": "string" }, + "wrappedAddress": { "type": "string" }, + "nativeToken": { "type": "string" }, + "wrappedToken": { "type": "string" } + }, + "required": ["fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/chains/solana/unwrap": { + "post": { + "tags": ["/chain/solana"], + "description": "Unwrap WSOL to SOL. Note: This closes the entire WSOL account, returning all WSOL as SOL.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "address": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "amount": { + "description": "The amount of WSOL to unwrap (in SOL, not lamports). If not provided, unwraps all WSOL.", + "type": "string", + "example": "1.0" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "string" }, + "amount": { "type": "string" }, + "wrappedAddress": { "type": "string" }, + "nativeToken": { "type": "string" }, + "wrappedToken": { "type": "string" } + }, + "required": ["fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/chains/ethereum/status": { + "get": { + "tags": ["/chain/ethereum"], + "description": "Get Ethereum chain status", + "parameters": [ + { + "schema": { + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon", "sepolia"], + "type": "string" + }, + "in": "query", + "name": "network", + "required": false, + "description": "The Ethereum network to use" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "chain": { "type": "string" }, + "network": { "type": "string" }, + "rpcUrl": { "type": "string" }, + "rpcProvider": { "type": "string" }, + "currentBlockNumber": { "type": "number" }, + "nativeCurrency": { "type": "string" }, + "swapProvider": { "type": "string" } + }, + "required": [ + "chain", + "network", + "rpcUrl", + "rpcProvider", + "currentBlockNumber", + "nativeCurrency", + "swapProvider" + ] + } + } + } + } + } + } + }, + "/chains/ethereum/estimate-gas": { + "get": { + "tags": ["/chain/ethereum"], + "description": "Estimate gas prices for Ethereum transactions", + "parameters": [ + { + "schema": { + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon", "sepolia"], + "type": "string" + }, + "in": "query", + "name": "network", + "required": false, + "description": "The Ethereum network to use" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "feePerComputeUnit": { "type": "number" }, + "denomination": { "type": "string" }, + "computeUnits": { "type": "number" }, + "feeAsset": { "type": "string" }, + "fee": { "type": "number" }, + "timestamp": { "type": "number" }, + "gasType": { "type": "string" }, + "maxFeePerGas": { "type": "number" }, + "maxPriorityFeePerGas": { "type": "number" } + }, + "required": ["feePerComputeUnit", "denomination", "computeUnits", "feeAsset", "fee", "timestamp"] + } + } + } + } + } + } + }, + "/chains/ethereum/balances": { + "post": { + "tags": ["/chain/ethereum"], + "description": "Get Ethereum balances. If no tokens specified or empty array provided, returns native token (ETH) and only non-zero balances for tokens from the token list. If specific tokens are requested, returns those exact tokens with their balances, including zeros.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Ethereum network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "sepolia" + ], + "type": "string" + }, + "address": { + "description": "Ethereum wallet address", + "default": "", + "type": "string" + }, + "tokens": { + "description": "A list of token symbols (ETH, USDC, WETH) or token addresses. Both formats are accepted and will be automatically detected. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances (with the exception of ETH).", + "type": "array", + "items": { "type": "string" }, + "example": ["ETH", "USDC", "WETH"] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "balances": { "type": "object", "additionalProperties": { "type": "number" } } }, + "required": ["balances"] + } + } + } + } + } + } + }, + "/chains/ethereum/poll": { + "post": { + "tags": ["/chain/ethereum"], + "description": "Poll Ethereum transaction status", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Ethereum network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "sepolia" + ], + "type": "string" + }, + "signature": { + "description": "Transaction hash to poll", + "type": "string", + "example": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + } + }, + "required": ["signature"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "currentBlock": { "type": "number" }, + "signature": { "type": "string" }, + "txBlock": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "txStatus": { "type": "number" }, + "fee": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "tokenBalanceChanges": { + "description": "Dictionary of token balance changes keyed by token input value (symbol or address)", + "type": "object", + "additionalProperties": { "type": "number" } + }, + "txData": { "anyOf": [{ "type": "object", "additionalProperties": {} }, { "type": "null" }] }, + "error": { "type": "string" } + }, + "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "txData"] + } + } + } + } + } + } + }, + "/chains/ethereum/allowances": { + "post": { + "tags": ["/chain/ethereum"], + "description": "Get token allowances", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Ethereum network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "sepolia" + ], + "type": "string" + }, + "address": { + "description": "Ethereum wallet address", + "default": "", + "type": "string" + }, + "spender": { + "description": "Connector name (e.g., uniswap/clmm, uniswap/amm, 0x/router) or contract address", + "type": "string", + "example": "uniswap/router" + }, + "tokens": { + "description": "Array of token symbols or addresses", + "type": "array", + "items": { "type": "string" }, + "example": ["USDC", "WETH"] + } + }, + "required": ["spender", "tokens"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "spender": { "type": "string" }, + "approvals": { "type": "object", "additionalProperties": { "type": "string" } } + }, + "required": ["spender", "approvals"] + } + } + } + } + } + } + }, + "/chains/ethereum/approve": { + "post": { + "tags": ["/chain/ethereum"], + "description": "Approve token spending", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Ethereum network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "sepolia" + ], + "type": "string" + }, + "address": { + "description": "Ethereum wallet address", + "default": "", + "type": "string" + }, + "spender": { + "description": "Connector name (e.g., uniswap/clmm, uniswap/amm, 0x/router) contract address", + "type": "string", + "example": "uniswap/router" + }, + "token": { "description": "Token symbol or address", "type": "string", "example": "USDC" }, + "amount": { + "description": "The amount to approve. If not provided, defaults to maximum amount (unlimited approval).", + "default": "", + "type": "string" + } + }, + "required": ["spender", "token"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "tokenAddress": { "type": "string" }, + "spender": { "type": "string" }, + "amount": { "type": "string" }, + "nonce": { "type": "number" }, + "fee": { "type": "string" } + }, + "required": ["tokenAddress", "spender", "amount", "nonce", "fee"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/chains/ethereum/wrap": { + "post": { + "tags": ["/chain/ethereum"], + "description": "Wrap native token to wrapped token (e.g., ETH to WETH, BNB to WBNB)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Ethereum network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "sepolia" + ], + "type": "string" + }, + "address": { + "description": "Ethereum wallet address", + "default": "", + "type": "string" + }, + "amount": { + "description": "The amount of native token to wrap (e.g., ETH, BNB, AVAX)", + "type": "string", + "example": "0.01" + } + }, + "required": ["amount"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "nonce": { "type": "number" }, + "fee": { "type": "string" }, + "amount": { "type": "string" }, + "wrappedAddress": { "type": "string" }, + "nativeToken": { "type": "string" }, + "wrappedToken": { "type": "string" } + }, + "required": ["nonce", "fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/chains/ethereum/unwrap": { + "post": { + "tags": ["/chain/ethereum"], + "description": "Unwrap wrapped token to native token (e.g., WETH to ETH, WBNB to BNB)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Ethereum network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "sepolia" + ], + "type": "string" + }, + "address": { + "description": "Ethereum wallet address", + "default": "", + "type": "string" + }, + "amount": { + "description": "The amount of wrapped token to unwrap (e.g., WETH, WBNB, WAVAX)", + "type": "string", + "example": "0.01" + } + }, + "required": ["amount"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "nonce": { "type": "number" }, + "fee": { "type": "string" }, + "amount": { "type": "string" }, + "wrappedAddress": { "type": "string" }, + "nativeToken": { "type": "string" }, + "wrappedToken": { "type": "string" } + }, + "required": ["nonce", "fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/chains/cosmos/balances": { + "post": { + "tags": ["/chain/cosmos"], + "description": "Get Cosmos balances. If no tokens specified or empty array provided, returns native token (OSMO) and only non-zero balances for tokens from the token list. If specific tokens are requested, returns those exact tokens with their balances, including zeros.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string", "example": "mainnet" }, + "address": { "type": "string", "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" }, + "tokens": { + "type": "array", + "items": { "type": "string" }, + "description": "A list of token symbols or addresses. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances plus the native token.", + "example": ["ATOM", "OSMO"] + }, + "fetchAll": { + "description": "fetch all tokens in wallet, not just those in token list (default: false)", + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "balances": { "type": "object", "additionalProperties": { "type": "number" } } }, + "required": ["balances"] + } + } + } + } + } + } + }, + "/chains/cosmos/status": { + "get": { + "tags": ["/chain/cosmos"], + "description": "Get cosmos chain status", + "parameters": [ + { + "schema": { "type": "string" }, + "examples": { "mainnet": { "value": "mainnet" }, "testnet": { "value": "testnet" } }, + "in": "query", + "name": "network", + "required": false + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "chain": { "type": "string" }, + "network": { "type": "string" }, + "rpcUrl": { "type": "string" }, + "rpcProvider": { "type": "string" }, + "currentBlockNumber": { "type": "number" }, + "nativeCurrency": { "type": "string" }, + "swapProvider": { "type": "string" } + }, + "required": [ + "chain", + "network", + "rpcUrl", + "rpcProvider", + "currentBlockNumber", + "nativeCurrency", + "swapProvider" + ] + } + } + } + } + } + } + }, + "/chains/cosmos/estimate-gas": { + "post": { + "tags": ["/chain/cosmos"], + "description": "Estimate gas prices for Cosmos transactions", + "requestBody": { + "content": { + "application/json": { + "schema": { "type": "object", "properties": { "network": { "type": "string", "example": "mainnet" } } } + } + } + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "feePerComputeUnit": { "type": "number" }, + "denomination": { "type": "string" }, + "computeUnits": { "type": "number" }, + "feeAsset": { "type": "string" }, + "fee": { "type": "number" }, + "timestamp": { "type": "number" }, + "gasType": { "type": "string" }, + "maxFeePerGas": { "type": "number" }, + "maxPriorityFeePerGas": { "type": "number" } + }, + "required": ["feePerComputeUnit", "denomination", "computeUnits", "feeAsset", "fee", "timestamp"] + } + } + } + } + } + } + }, + "/chains/cosmos/poll": { + "post": { + "tags": ["/chain/cosmos"], + "description": "Poll Cosmos transaction status", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string", "example": "mainnet" }, + "signature": { + "type": "string", + "example": "344A0C038C05D1FA938E78828925109879E30C397100BD84D0BA08A463B2FF82" + }, + "tokens": { + "description": "Array of token symbols or addresses for balance change calculation", + "type": "array", + "items": { "type": "string" } + }, + "walletAddress": { + "description": "Wallet address for balance change calculation (required if tokens provided)", + "type": "string" + } + }, + "required": ["signature"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "currentBlock": { "type": "number" }, + "signature": { "type": "string" }, + "txBlock": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "txStatus": { "type": "number" }, + "fee": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "tokenBalanceChanges": { + "description": "Dictionary of token balance changes keyed by token input value (symbol or address)", + "type": "object", + "additionalProperties": { "type": "number" } + }, + "txData": { "anyOf": [{ "type": "object", "additionalProperties": {} }, { "type": "null" }] }, + "error": { "type": "string" } + }, + "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "txData"] + } + } + } + } + } + } + }, + "/chains/cosmos/tokens": { + "get": { + "tags": ["/chain/cosmos"], + "description": "Get Cosmos/Osmosis tokens", + "parameters": [ + { + "schema": { "type": "string" }, + "examples": { "mainnet": { "value": "mainnet" }, "testnet": { "value": "testnet" } }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, + "in": "query", + "name": "tokenSymbols", + "required": false + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tokens": { + "type": "array", + "items": { + "type": "object", + "properties": { + "symbol": { "type": "string" }, + "address": { "type": "string" }, + "decimals": { "type": "number" }, + "name": { "type": "string" } + }, + "required": ["symbol", "address", "decimals", "name"] + } + } + }, + "required": ["tokens"] + } + } + } + } + } + } + }, + "/connectors/jupiter/router/quote-swap": { + "get": { + "tags": ["/connector/jupiter"], + "description": "Get an executable swap quote from Jupiter", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "SOL", + "in": "query", + "name": "baseToken", + "required": true, + "description": "Solana token symbol or address to determine swap direction" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": true, + "description": "The other Solana token symbol or address in the pair" + }, + { + "schema": { "type": "number" }, + "example": 0.1, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount of base token to trade" + }, + { + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + }, + { + "schema": { "default": true, "type": "boolean" }, + "in": "query", + "name": "restrictIntermediateTokens", + "required": false, + "description": "Restrict routing through highly liquid intermediate tokens only for better price and stability" + }, + { + "schema": { "default": false, "type": "boolean" }, + "in": "query", + "name": "onlyDirectRoutes", + "required": false, + "description": "Restrict routing to only go through 1 market" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "minAmountOut": { + "description": "Minimum amount of tokenOut that will be accepted", + "type": "number" + }, + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "quoteResponse": { + "type": "object", + "properties": { + "inputMint": { "description": "Solana mint address of input token", "type": "string" }, + "inAmount": { "description": "Input amount in token decimals", "type": "string" }, + "outputMint": { "description": "Solana mint address of output token", "type": "string" }, + "outAmount": { "description": "Expected output amount in token decimals", "type": "string" }, + "otherAmountThreshold": { + "description": "Minimum output amount based on slippage", + "type": "string" + }, + "swapMode": { "description": "Swap mode used (ExactIn or ExactOut)", "type": "string" }, + "slippageBps": { "description": "Slippage in basis points", "type": "number" }, + "platformFee": { "description": "Platform fee information if applicable" }, + "priceImpactPct": { "description": "Estimated price impact percentage", "type": "string" }, + "routePlan": { + "description": "Detailed routing plan through various markets", + "type": "array", + "items": {} + }, + "contextSlot": { "description": "Solana slot used for quote calculation", "type": "number" }, + "timeTaken": { "description": "Time taken to generate quote in milliseconds", "type": "number" } + }, + "required": [ + "inputMint", + "inAmount", + "outputMint", + "outAmount", + "otherAmountThreshold", + "swapMode", + "slippageBps", + "priceImpactPct", + "routePlan" + ] + }, + "approximation": { + "description": "Indicates if ExactIn approximation was used when ExactOut route was not available", + "type": "boolean" + } + }, + "required": [ + "quoteId", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "priceImpactPct", + "minAmountOut", + "maxAmountIn", + "quoteResponse" + ] + } + } + } + } + } + } + }, + "/connectors/jupiter/router/execute-quote": { + "post": { + "tags": ["/connector/jupiter"], + "description": "Execute a previously fetched quote from Jupiter", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { + "description": "Solana wallet address that will execute the swap", + "default": "", + "type": "string" + }, + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "quoteId": { + "description": "ID of the Jupiter quote to execute", + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "priorityLevel": { + "description": "Priority level for Solana transaction processing", + "enum": ["medium", "high", "veryHigh"], + "default": "veryHigh", + "type": "string" + }, + "maxLamports": { + "description": "Maximum priority fee in lamports for Solana transaction", + "default": [1000000], + "type": "number" + } + }, + "required": ["quoteId"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/jupiter/router/execute-swap": { + "post": { + "tags": ["/connector/jupiter"], + "description": "Quote and execute a token swap on Jupiter in one step", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { + "description": "Solana wallet address that will execute the swap", + "default": "", + "type": "string" + }, + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "baseToken": { + "description": "Solana token symbol or address to determine swap direction", + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "description": "The other Solana token symbol or address in the pair", + "type": "string", + "example": "USDC" + }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.1 }, + "side": { + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 1, + "type": "number" + }, + "restrictIntermediateTokens": { + "description": "Restrict routing through highly liquid intermediate tokens only for better price and stability", + "default": true, + "type": "boolean" + }, + "onlyDirectRoutes": { + "description": "Restrict routing to only go through 1 market", + "default": false, + "type": "boolean" + }, + "priorityLevel": { + "description": "Priority level for Solana transaction processing", + "enum": ["medium", "high", "veryHigh"], + "default": "veryHigh", + "type": "string" + }, + "maxLamports": { + "description": "Maximum priority fee in lamports for Solana transaction", + "default": 1000000, + "type": "number" + } + }, + "required": ["baseToken", "quoteToken", "amount", "side"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/fetch-pools": { + "get": { + "tags": ["/connector/meteora"], + "description": "Fetch info about Meteora pools", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "minimum": 1, "default": 10, "type": "number" }, + "example": 10, + "in": "query", + "name": "limit", + "required": false, + "description": "Maximum number of pools to return" + }, + { + "schema": { "type": "string" }, + "example": "SOL", + "in": "query", + "name": "tokenA", + "required": false, + "description": "First token symbol or address" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "tokenB", + "required": false, + "description": "Second token symbol or address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" } + }, + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount", + "activeBinId" + ], + "title": "PoolInfo" + } + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/pool-info": { + "get": { + "tags": ["/connector/meteora"], + "description": "Get pool information for a Meteora pool", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Meteora DLMM pool address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" }, + "dynamicFeePct": { "type": "number" }, + "minBinId": { "type": "number" }, + "maxBinId": { "type": "number" }, + "bins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "binId": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"], + "title": "BinLiquidity" + } + } + }, + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount", + "activeBinId", + "dynamicFeePct", + "minBinId", + "maxBinId", + "bins" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/positions-owned": { + "get": { + "tags": ["/connector/meteora"], + "description": "Retrieve all positions owned by a user's wallet across all Meteora pools", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "", + "in": "query", + "name": "walletAddress", + "required": true, + "description": "Solana wallet address to check for positions" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/position-info": { + "get": { + "tags": ["/connector/meteora"], + "description": "Get details for a specific Meteora position", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "", + "in": "query", + "name": "positionAddress", + "required": true, + "description": "Position NFT address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/quote-position": { + "get": { + "tags": ["/connector/meteora"], + "description": "Quote amounts for a new Meteora CLMM position", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "number" }, + "example": 150, + "in": "query", + "name": "lowerPrice", + "required": true, + "description": "Lower price bound for the position" + }, + { + "schema": { "type": "number" }, + "example": 250, + "in": "query", + "name": "upperPrice", + "required": true, + "description": "Upper price bound for the position" + }, + { + "schema": { "type": "string" }, + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Meteora DLMM pool address" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "baseTokenAmount", + "required": false, + "description": "Amount of base token to deposit" + }, + { + "schema": { "type": "number" }, + "example": 2, + "in": "query", + "name": "quoteTokenAmount", + "required": false, + "description": "Amount of quote token to deposit" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + }, + { + "schema": { "enum": [0, 1, 2], "type": "number" }, + "example": 0, + "in": "query", + "name": "strategyType", + "required": false, + "description": "Strategy type for the position" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, + "liquidity": {} + }, + "required": [ + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/quote-swap": { + "get": { + "tags": ["/connector/meteora"], + "description": "Get swap quote for Meteora CLMM", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", + "in": "query", + "name": "poolAddress", + "required": false, + "description": "Meteora DLMM pool address (optional - can be looked up from baseToken and quoteToken)" + }, + { + "schema": { "type": "string" }, + "example": "SOL", + "in": "query", + "name": "baseToken", + "required": true, + "description": "Token to determine swap direction" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false, + "description": "The other token in the pair (optional - required if poolAddress not provided)" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount to swap" + }, + { + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "example": "SELL", + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } + }, + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/execute-swap": { + "post": { + "tags": ["/connector/meteora"], + "description": "Execute a token swap on Meteora DLMM", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will execute the swap", + "default": "", + "type": "string", + "example": "" + }, + "poolAddress": { + "description": "Meteora DLMM pool address (optional - can be looked up from baseToken and quoteToken)", + "type": "string", + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3" + }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "quoteToken": { + "description": "Quote token symbol or address (optional - required if poolAddress not provided)", + "type": "string", + "example": "USDC" + }, + "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, + "side": { + "description": "Trade direction", + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string", + "example": "SELL" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + } + }, + "required": ["baseToken", "amount", "side"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/open-position": { + "post": { + "tags": ["/connector/meteora"], + "description": "Open a new Meteora position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will open the position", + "default": "", + "type": "string", + "example": "" + }, + "lowerPrice": { + "description": "Lower price bound for the position", + "type": "number", + "example": 150 + }, + "upperPrice": { + "description": "Upper price bound for the position", + "type": "number", + "example": 250 + }, + "poolAddress": { + "description": "Meteora DLMM pool address", + "type": "string", + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3" + }, + "baseTokenAmount": { + "description": "Amount of base token to deposit", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { + "description": "Amount of quote token to deposit", + "type": "number", + "example": 2 + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + }, + "strategyType": { + "description": "Strategy type for the position", + "enum": [0, 1, 2], + "type": "number", + "example": 0 + } + }, + "required": ["lowerPrice", "upperPrice", "poolAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": [ + "fee", + "positionAddress", + "positionRent", + "baseTokenAmountAdded", + "quoteTokenAmountAdded" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/add-liquidity": { + "post": { + "tags": ["/connector/meteora"], + "description": "Add liquidity to a Meteora position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will add liquidity", + "default": "", + "type": "string", + "example": "" + }, + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + }, + "baseTokenAmount": { + "description": "Amount of base token to deposit", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { + "description": "Amount of quote token to deposit", + "type": "number", + "example": 2 + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + }, + "strategyType": { + "description": "Strategy type for the position", + "enum": [0, 1, 2], + "type": "number", + "example": 0 + } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" }, + "newPositionAddress": { "type": "string" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/remove-liquidity": { + "post": { + "tags": ["/connector/meteora"], + "description": "Remove liquidity from a Meteora position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will remove liquidity", + "default": "", + "type": "string", + "example": "" + }, + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + }, + "liquidityPct": { + "minimum": 0, + "maximum": 100, + "description": "Percentage of liquidity to remove", + "default": 100, + "type": "number", + "example": 100 + } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/collect-fees": { + "post": { + "tags": ["/connector/meteora"], + "description": "Collect fees from a Meteora position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will collect fees", + "default": "", + "type": "string", + "example": "" + }, + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/close-position": { + "post": { + "tags": ["/connector/meteora"], + "description": "Close a Meteora position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will close the position", + "default": "", + "type": "string", + "example": "" + }, + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": [ + "fee", + "positionRentRefunded", + "baseTokenAmountRemoved", + "quoteTokenAmountRemoved", + "baseFeeAmountCollected", + "quoteFeeAmountCollected" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/raydium/amm/pool-info": { + "get": { + "tags": ["/connector/raydium"], + "description": "Get AMM pool information from Raydium", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Raydium AMM pool address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount" + ] + } + } + } + } + } + } + }, + "/connectors/raydium/amm/position-info": { + "get": { + "tags": ["/connector/raydium"], + "description": "Get info about a Raydium AMM position", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Raydium AMM pool address" + }, + { + "schema": { "default": "", "type": "string" }, + "in": "query", + "name": "walletAddress", + "required": false, + "description": "Solana wallet address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "walletAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "lpTokenAmount": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "price": { "type": "number" } + }, + "required": [ + "poolAddress", + "walletAddress", + "baseTokenAddress", + "quoteTokenAddress", + "lpTokenAmount", + "baseTokenAmount", + "quoteTokenAmount", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/raydium/amm/quote-liquidity": { + "get": { + "tags": ["/connector/raydium"], + "description": "Quote amounts for a new Raydium AMM liquidity position", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Raydium AMM pool address" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "baseTokenAmount", + "required": true, + "description": "Amount of base token to add" + }, + { + "schema": { "type": "number" }, + "example": 2, + "in": "query", + "name": "quoteTokenAmount", + "required": true, + "description": "Amount of quote token to add" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" } + }, + "required": [ + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" + ] + } + } + } + }, + "500": { + "description": "Default Response", + "content": { + "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } + } + } + } + } + }, + "/connectors/raydium/amm/quote-swap": { + "get": { + "tags": ["/connector/raydium"], + "description": "Get swap quote for Raydium AMM", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", + "in": "query", + "name": "poolAddress", + "required": false, + "description": "AMM pool address (optional - can be looked up from baseToken and quoteToken)" + }, + { + "schema": { "type": "string" }, + "example": "SOL", + "in": "query", + "name": "baseToken", + "required": true, + "description": "Token to determine swap direction" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false, + "description": "The other token in the pair (optional - required if poolAddress not provided)" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount to swap" + }, + { + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } + }, + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] + } + } + } + } + } + } + }, + "/connectors/raydium/amm/execute-swap": { + "post": { + "tags": ["/connector/raydium"], + "description": "Execute a swap on Raydium AMM or CPMM", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { "type": "string", "example": "" }, + "network": { "type": "string", "default": "mainnet-beta" }, + "poolAddress": { "type": "string", "example": "" }, + "baseToken": { "type": "string", "example": "SOL" }, + "quoteToken": { "type": "string", "example": "USDC" }, + "amount": { "type": "number", "example": 0.01 }, + "side": { "type": "string", "example": "SELL" }, + "slippagePct": { "type": "number", "example": 1 } + }, + "required": ["baseToken", "amount", "side"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/raydium/amm/add-liquidity": { + "post": { + "tags": ["/connector/raydium"], + "description": "Add liquidity to a Raydium AMM/CPMM pool", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "poolAddress": { + "description": "Raydium AMM pool address", + "type": "string", + "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" + }, + "baseTokenAmount": { + "description": "Amount of base token to add", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + } + }, + "required": ["poolAddress", "baseTokenAmount", "quoteTokenAmount"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/raydium/amm/remove-liquidity": { + "post": { + "tags": ["/connector/raydium"], + "description": "Remove liquidity from a Raydium AMM/CPMM pool", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "poolAddress": { + "description": "Raydium AMM pool address", + "type": "string", + "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" + }, + "percentageToRemove": { + "minimum": 0, + "maximum": 100, + "description": "Percentage of liquidity to remove", + "type": "number", + "example": 100 + } + }, + "required": ["poolAddress", "percentageToRemove"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/pool-info": { + "get": { + "tags": ["/connector/raydium"], + "description": "Get CLMM pool information from Raydium", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Raydium CLMM pool address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" } + }, + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount", + "activeBinId" + ] + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/positions-owned": { + "get": { + "tags": ["/connector/raydium"], + "description": "Retrieve all positions owned by a user's wallet across all Raydium CLMM pools", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "", + "in": "query", + "name": "walletAddress", + "required": true, + "description": "Solana wallet address to check for positions" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/position-info": { + "get": { + "tags": ["/connector/raydium"], + "description": "Get info about a Raydium CLMM position", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "", + "in": "query", + "name": "positionAddress", + "required": true, + "description": "Position NFT address" + }, + { + "schema": { "default": "", "type": "string" }, + "in": "query", + "name": "walletAddress", + "required": false, + "description": "Solana wallet address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/quote-position": { + "get": { + "tags": ["/connector/raydium"], + "description": "Quote amounts for a new Raydium CLMM position", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "number" }, + "example": 100, + "in": "query", + "name": "lowerPrice", + "required": true, + "description": "Lower price bound for the position" + }, + { + "schema": { "type": "number" }, + "example": 300, + "in": "query", + "name": "upperPrice", + "required": true, + "description": "Upper price bound for the position" + }, + { + "schema": { "type": "string" }, + "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Raydium CLMM pool address" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "baseTokenAmount", + "required": false, + "description": "Amount of base token to deposit" + }, + { + "schema": { "type": "number" }, + "example": 2, + "in": "query", + "name": "quoteTokenAmount", + "required": false, + "description": "Amount of quote token to deposit" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, + "liquidity": {} + }, + "required": [ + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" + ] + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/quote-swap": { + "get": { + "tags": ["/connector/raydium"], + "description": "Get swap quote for Raydium CLMM", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", + "in": "query", + "name": "poolAddress", + "required": false, + "description": "CLMM pool address (optional - can be looked up from tokens)" + }, + { + "schema": { "type": "string" }, + "example": "SOL", + "in": "query", + "name": "baseToken", + "required": true, + "description": "Token to determine swap direction" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false, + "description": "The other token in the pair" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount to swap" + }, + { + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } + }, + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/execute-swap": { + "post": { + "tags": ["/connector/raydium"], + "description": "Execute a swap on Raydium CLMM", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { + "description": "Solana wallet address", + "default": "", + "type": "string", + "example": "" + }, + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "poolAddress": { + "description": "CLMM pool address (optional)", + "type": "string", + "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv" + }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, + "side": { + "description": "Trade direction", + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string", + "example": "SELL" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + } + }, + "required": ["baseToken", "amount", "side"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/open-position": { + "post": { + "tags": ["/connector/raydium"], + "description": "Open a new Raydium CLMM position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "lowerPrice": { + "description": "Lower price bound for the position", + "type": "number", + "example": 100 + }, + "upperPrice": { + "description": "Upper price bound for the position", + "type": "number", + "example": 300 + }, + "poolAddress": { + "description": "Raydium CLMM pool address", + "type": "string", + "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv" + }, + "baseTokenAmount": { + "description": "Amount of base token to deposit", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { + "description": "Amount of quote token to deposit", + "type": "number", + "example": 2 + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + } + }, + "required": ["lowerPrice", "upperPrice", "poolAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": [ + "fee", + "positionAddress", + "positionRent", + "baseTokenAmountAdded", + "quoteTokenAmountAdded" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/add-liquidity": { + "post": { + "tags": ["/connector/raydium"], + "description": "Add liquidity to existing Raydium CLMM position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + }, + "baseTokenAmount": { + "description": "Amount of base token to add", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + } + }, + "required": ["positionAddress", "baseTokenAmount", "quoteTokenAmount"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" }, + "newPositionAddress": { "type": "string" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/remove-liquidity": { + "post": { + "tags": ["/connector/raydium"], + "description": "Remove liquidity from Raydium CLMM position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "positionAddress": { + "description": "Position NFT address to remove liquidity from", + "type": "string", + "example": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" + }, + "percentageToRemove": { + "minimum": 0, + "maximum": 100, + "description": "Percentage of liquidity to remove", + "type": "number", + "example": 100 + } + }, + "required": ["positionAddress", "percentageToRemove"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/collect-fees": { + "post": { + "tags": ["/connector/raydium"], + "description": "Collect fees from a Raydium CLMM position by removing 1% of liquidity", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string", "default": "mainnet-beta" }, + "walletAddress": { "type": "string", "example": "" }, + "positionAddress": { "type": "string" } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/raydium/clmm/close-position": { + "post": { + "tags": ["/connector/raydium"], + "description": "Close a Raydium CLMM position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "positionAddress": { + "description": "Position NFT address to close", + "type": "string", + "example": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" + } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": [ + "fee", + "positionRentRefunded", + "baseTokenAmountRemoved", + "quoteTokenAmountRemoved", + "baseFeeAmountCollected", + "quoteFeeAmountCollected" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/uniswap/router/quote-swap": { + "get": { + "tags": ["/connector/uniswap"], + "description": "Get an executable swap quote from Uniswap Universal Router", + "parameters": [ + { + "schema": { + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "type": "string" + }, + "in": "query", + "name": "network", + "required": false, + "description": "The EVM network to use" + }, + { + "schema": { "type": "string" }, + "example": "WETH", + "in": "query", + "name": "baseToken", + "required": true, + "description": "First token in the trading pair" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": true, + "description": "Second token in the trading pair" + }, + { + "schema": { "type": "number" }, + "example": 0.001, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount of base token to trade" + }, + { + "schema": { "enum": ["BUY", "SELL"], "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + }, + { + "schema": { "default": "", "type": "string" }, + "in": "query", + "name": "walletAddress", + "required": false, + "description": "Wallet address for more accurate quotes (optional)" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "minAmountOut": { + "description": "Minimum amount of tokenOut that will be accepted", + "type": "number" + }, + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "routePath": { "description": "Human-readable route path", "type": "string" } + }, + "required": [ + "quoteId", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "priceImpactPct", + "minAmountOut", + "maxAmountIn" + ] + } + } + } + } + } + } + }, + "/connectors/uniswap/router/execute-quote": { + "post": { + "tags": ["/connector/uniswap"], + "description": "Execute a previously fetched quote from Uniswap Universal Router", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { + "description": "Wallet address that will execute the swap", + "default": "", + "type": "string", + "example": "" + }, + "network": { + "description": "The blockchain network to use", + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "type": "string" + }, + "quoteId": { + "description": "ID of the quote to execute", + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + } + }, + "required": ["quoteId"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/uniswap/router/execute-swap": { + "post": { + "tags": ["/connector/uniswap"], + "description": "Quote and execute a token swap on Uniswap Universal Router in one step", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { + "description": "Wallet address that will execute the swap", "default": "", + "type": "string", + "example": "" + }, + "network": { + "description": "The blockchain network to use", + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], "type": "string" }, - "spender": { - "description": "Connector name (e.g., uniswap/clmm, uniswap/amm, 0x/router) or contract address", + "baseToken": { + "description": "Token to determine swap direction", "type": "string", - "example": "uniswap/router" + "example": "WETH" }, - "tokens": { - "description": "Array of token symbols or addresses", - "type": "array", - "items": { "type": "string" }, - "example": ["USDC", "WETH"] + "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.001 }, + "side": { + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", + "enum": ["BUY", "SELL"], + "type": "string" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 1 } }, - "required": ["spender", "tokens"] + "required": ["baseToken", "quoteToken", "amount", "side"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/uniswap/amm/pool-info": { + "get": { + "tags": ["/connector/uniswap"], + "description": "Get AMM pool information from Uniswap V2", + "parameters": [ + { + "schema": { + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "type": "string" + }, + "in": "query", + "name": "network", + "required": false, + "description": "The EVM network to use" + }, + { + "schema": { "type": "string" }, + "example": "0x88A43bbDF9D098eEC7bCEda4e2494615dfD9bB9C", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Uniswap V2 pool address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount" + ] + } + } + } + } + } + } + }, + "/connectors/uniswap/amm/position-info": { + "get": { + "tags": ["/connector/uniswap"], + "description": "Get position information for a Uniswap V2 pool", + "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { + "schema": { "type": "string" }, + "example": "", + "in": "query", + "name": "walletAddress", + "required": false + }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "walletAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "lpTokenAmount": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "price": { "type": "number" } + }, + "required": [ + "poolAddress", + "walletAddress", + "baseTokenAddress", + "quoteTokenAddress", + "lpTokenAmount", + "baseTokenAmount", + "quoteTokenAmount", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/uniswap/amm/quote-swap": { + "get": { + "tags": ["/connector/uniswap"], + "description": "Get swap quote for Uniswap V2 AMM", + "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": false }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, + { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, + { + "schema": { "type": "string", "enum": ["BUY", "SELL"] }, + "example": "SELL", + "in": "query", + "name": "side", + "required": true + }, + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } + }, + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] + } } } + } + } + } + }, + "/connectors/uniswap/amm/quote-liquidity": { + "get": { + "tags": ["/connector/uniswap"], + "description": "Get liquidity quote for a Uniswap V2 pool", + "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, + { + "schema": { "type": "number" }, + "example": 0.001, + "in": "query", + "name": "baseTokenAmount", + "required": true }, - "required": true - }, + { + "schema": { "type": "number" }, + "example": 2.5, + "in": "query", + "name": "quoteTokenAmount", + "required": true + }, + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } + ], "responses": { "200": { "description": "Default Response", @@ -1421,10 +6924,19 @@ "schema": { "type": "object", "properties": { - "spender": { "type": "string" }, - "approvals": { "type": "object", "additionalProperties": { "type": "string" } } + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" } }, - "required": ["spender", "approvals"] + "required": [ + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" + ] } } } @@ -1432,50 +6944,45 @@ } } }, - "/chains/ethereum/approve": { + "/connectors/uniswap/amm/execute-swap": { "post": { - "tags": ["/chain/ethereum"], - "description": "Approve token spending", + "tags": ["/connector/uniswap"], + "description": "Execute a swap on Uniswap V2 AMM using Router02", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { - "description": "The Ethereum network to use", - "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" - ], - "type": "string" - }, - "address": { - "description": "Ethereum wallet address", + "walletAddress": { + "description": "Wallet address that will execute the swap", "default": "", "type": "string" }, - "spender": { - "description": "Connector name (e.g., uniswap/clmm, uniswap/amm, 0x/router) contract address", - "type": "string", - "example": "uniswap/router" + "network": { + "description": "The EVM network to use", + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "type": "string" }, - "token": { "description": "Token symbol or address", "type": "string", "example": "USDC" }, - "amount": { - "description": "The amount to approve. If not provided, defaults to maximum amount (unlimited approval).", + "poolAddress": { + "description": "Pool address (optional - can be looked up from tokens)", "default": "", "type": "string" + }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "WETH" }, + "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount to swap", "type": "number", "example": 0.001 }, + "side": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number" } }, - "required": ["spender", "token"] + "required": ["baseToken", "amount", "side"] } } }, @@ -1489,18 +6996,37 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenAddress": { "type": "string" }, - "spender": { "type": "string" }, - "amount": { "type": "string" }, - "nonce": { "type": "number" }, - "fee": { "type": "string" } + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } }, - "required": ["tokenAddress", "spender", "amount", "nonce", "fee"] + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] } }, "required": ["signature", "status"] @@ -1511,61 +7037,10 @@ } } }, - "/chains/ethereum/estimate-gas": { - "post": { - "tags": ["/chain/ethereum"], - "description": "Estimate gas prices for Ethereum transactions", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "network": { - "description": "The Ethereum network to use", - "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" - ], - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "feePerComputeUnit": { "type": "number" }, - "denomination": { "type": "string" }, - "timestamp": { "type": "number" } - }, - "required": ["feePerComputeUnit", "denomination", "timestamp"] - } - } - } - } - } - } - }, - "/chains/ethereum/wrap": { + "/connectors/uniswap/amm/add-liquidity": { "post": { - "tags": ["/chain/ethereum"], - "description": "Wrap native token to wrapped token (e.g., ETH to WETH, BNB to WBNB)", + "tags": ["/connector/uniswap"], + "description": "Add liquidity to a Uniswap V2 pool", "requestBody": { "content": { "application/json": { @@ -1573,33 +7048,34 @@ "type": "object", "properties": { "network": { - "description": "The Ethereum network to use", + "description": "The EVM network to use", "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" - ], + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], "type": "string" }, - "address": { - "description": "Ethereum wallet address", + "walletAddress": { + "description": "Wallet address that will add liquidity", "default": "", "type": "string" }, - "amount": { - "description": "The amount of native token to wrap (e.g., ETH, BNB, AVAX)", - "type": "string", - "example": "0.01" + "poolAddress": { "description": "Address of the Uniswap V2 pool", "type": "string" }, + "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number" + }, + "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, + "maxGas": { + "description": "Maximum gas limit for the transaction", + "type": "number", + "example": 300000 } }, - "required": ["amount"] + "required": ["poolAddress", "baseTokenAmount", "quoteTokenAmount"] } } }, @@ -1618,14 +7094,11 @@ "data": { "type": "object", "properties": { - "nonce": { "type": "number" }, - "fee": { "type": "string" }, - "amount": { "type": "string" }, - "wrappedAddress": { "type": "string" }, - "nativeToken": { "type": "string" }, - "wrappedToken": { "type": "string" } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, - "required": ["nonce", "fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } }, "required": ["signature", "status"] @@ -1636,10 +7109,10 @@ } } }, - "/chains/ethereum/unwrap": { + "/connectors/uniswap/amm/remove-liquidity": { "post": { - "tags": ["/chain/ethereum"], - "description": "Unwrap wrapped token to native token (e.g., WETH to ETH, WBNB to BNB)", + "tags": ["/connector/uniswap"], + "description": "Remove liquidity from a Uniswap V2 pool", "requestBody": { "content": { "application/json": { @@ -1647,33 +7120,31 @@ "type": "object", "properties": { "network": { - "description": "The Ethereum network to use", + "description": "The EVM network to use", "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" - ], + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], "type": "string" }, - "address": { - "description": "Ethereum wallet address", + "walletAddress": { + "description": "Wallet address that will remove liquidity", "default": "", "type": "string" }, - "amount": { - "description": "The amount of wrapped token to unwrap (e.g., WETH, WBNB, WAVAX)", - "type": "string", - "example": "0.01" + "poolAddress": { "description": "Address of the Uniswap V2 pool", "type": "string" }, + "percentageToRemove": { + "minimum": 0, + "maximum": 100, + "description": "Percentage of liquidity to remove", + "type": "number" + }, + "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, + "maxGas": { + "description": "Maximum gas limit for the transaction", + "type": "number", + "example": 300000 } }, - "required": ["amount"] + "required": ["poolAddress", "percentageToRemove"] } } }, @@ -1692,14 +7163,11 @@ "data": { "type": "object", "properties": { - "nonce": { "type": "number" }, - "fee": { "type": "string" }, - "amount": { "type": "string" }, - "wrappedAddress": { "type": "string" }, - "nativeToken": { "type": "string" }, - "wrappedToken": { "type": "string" } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, - "required": ["nonce", "fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } }, "required": ["signature", "status"] @@ -1710,69 +7178,236 @@ } } }, - "/connectors/jupiter/router/quote-swap": { + "/connectors/uniswap/clmm/pool-info": { "get": { - "tags": ["/connector/jupiter"], - "description": "Get an executable swap quote from Jupiter", + "tags": ["/connector/uniswap"], + "description": "Get CLMM pool information from Uniswap V3", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "type": "string" + }, "in": "query", "name": "network", "required": false, - "description": "Solana network to use" + "description": "The EVM network to use" }, { "schema": { "type": "string" }, - "example": "SOL", + "example": "0xd0b53d9277642d899df5c87a3966a349a798f224", "in": "query", - "name": "baseToken", + "name": "poolAddress", "required": true, - "description": "Solana token symbol or address to determine swap direction" - }, + "description": "Uniswap V3 pool address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" } + }, + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount", + "activeBinId" + ] + } + } + } + } + } + } + }, + "/connectors/uniswap/clmm/position-info": { + "get": { + "tags": ["/connector/uniswap"], + "description": "Get position information for a Uniswap V3 position", + "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { "schema": { "type": "string" }, - "example": "USDC", + "example": "1234", "in": "query", - "name": "quoteToken", + "name": "positionAddress", "required": true, - "description": "The other Solana token symbol or address in the pair" + "description": "Position NFT token ID" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/uniswap/clmm/positions-owned": { + "get": { + "tags": ["/connector/uniswap"], + "description": "Get all Uniswap V3 positions owned by a wallet", + "parameters": [ + { + "schema": { "default": "base", "type": "string" }, + "example": "base", + "in": "query", + "name": "network", + "required": false }, { - "schema": { "type": "number" }, - "example": 0.1, + "schema": { "type": "string" }, + "example": "", "in": "query", - "name": "amount", - "required": true, - "description": "Amount of base token to trade" + "name": "walletAddress", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + } + }, + "/connectors/uniswap/clmm/quote-position": { + "get": { + "tags": ["/connector/uniswap"], + "description": "Get a quote for opening a position on Uniswap V3", + "parameters": [ + { + "schema": { "type": "string", "default": "base" }, + "example": "base", + "in": "query", + "name": "network", + "required": false }, + { "schema": { "type": "number" }, "example": 2000, "in": "query", "name": "lowerPrice", "required": true }, + { "schema": { "type": "number" }, "example": 4000, "in": "query", "name": "upperPrice", "required": true }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { "type": "string", "default": "0xd0b53d9277642d899df5c87a3966a349a798f224" }, + "example": "0xd0b53d9277642d899df5c87a3966a349a798f224", "in": "query", - "name": "side", - "required": true, - "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" + "name": "poolAddress", + "required": true }, { - "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "schema": { "type": "number" }, + "example": 0.001, "in": "query", - "name": "slippagePct", - "required": false, - "description": "Maximum acceptable slippage percentage" + "name": "baseTokenAmount", + "required": false }, { - "schema": { "default": true, "type": "boolean" }, + "schema": { "type": "number" }, + "example": 3, "in": "query", - "name": "restrictIntermediateTokens", - "required": false, - "description": "Restrict routing through highly liquid intermediate tokens only for better price and stability" + "name": "quoteTokenAmount", + "required": false }, { - "schema": { "default": false, "type": "boolean" }, + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, "in": "query", - "name": "onlyDirectRoutes", - "required": false, - "description": "Restrict routing to only go through 1 market" + "name": "slippagePct", + "required": false } ], "responses": { @@ -1783,65 +7418,19 @@ "schema": { "type": "object", "properties": { - "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, - "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, - "minAmountOut": { - "description": "Minimum amount of tokenOut that will be accepted", - "type": "number" - }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, - "quoteResponse": { - "type": "object", - "properties": { - "inputMint": { "description": "Solana mint address of input token", "type": "string" }, - "inAmount": { "description": "Input amount in token decimals", "type": "string" }, - "outputMint": { "description": "Solana mint address of output token", "type": "string" }, - "outAmount": { "description": "Expected output amount in token decimals", "type": "string" }, - "otherAmountThreshold": { - "description": "Minimum output amount based on slippage", - "type": "string" - }, - "swapMode": { "description": "Swap mode used (ExactIn or ExactOut)", "type": "string" }, - "slippageBps": { "description": "Slippage in basis points", "type": "number" }, - "platformFee": { "description": "Platform fee information if applicable" }, - "priceImpactPct": { "description": "Estimated price impact percentage", "type": "string" }, - "routePlan": { - "description": "Detailed routing plan through various markets", - "type": "array", - "items": {} - }, - "contextSlot": { "description": "Solana slot used for quote calculation", "type": "number" }, - "timeTaken": { "description": "Time taken to generate quote in milliseconds", "type": "number" } - }, - "required": [ - "inputMint", - "inAmount", - "outputMint", - "outAmount", - "otherAmountThreshold", - "swapMode", - "slippageBps", - "priceImpactPct", - "routePlan" - ] - } + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, + "liquidity": {} }, "required": [ - "quoteId", - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "price", - "priceImpactPct", - "minAmountOut", - "maxAmountIn", - "quoteResponse" + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" ] } } @@ -1850,50 +7439,31 @@ } } }, - "/connectors/jupiter/router/execute-quote": { - "post": { - "tags": ["/connector/jupiter"], - "description": "Execute a previously fetched quote from Jupiter", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "walletAddress": { - "description": "Solana wallet address that will execute the swap", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "type": "string" - }, - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "quoteId": { - "description": "ID of the Jupiter quote to execute", - "type": "string", - "example": "123e4567-e89b-12d3-a456-426614174000" - }, - "priorityLevel": { - "description": "Priority level for Solana transaction processing", - "enum": ["medium", "high", "veryHigh"], - "default": "veryHigh", - "type": "string" - }, - "maxLamports": { - "description": "Maximum priority fee in lamports for Solana transaction", - "default": [1000000], - "type": "number" - } - }, - "required": ["quoteId"] - } - } + "/connectors/uniswap/clmm/quote-swap": { + "get": { + "tags": ["/connector/uniswap"], + "description": "Get swap quote for Uniswap V3 CLMM", + "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { + "schema": { "type": "string" }, + "in": "query", + "name": "poolAddress", + "required": false, + "description": "Pool address (optional - can be looked up from baseToken and quoteToken)" }, - "required": true - }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, + { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, + { + "schema": { "type": "string", "enum": ["BUY", "SELL"] }, + "example": "SELL", + "in": "query", + "name": "side", + "required": true + }, + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } + ], "responses": { "200": { "description": "Default Response", @@ -1902,40 +7472,28 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, - "status": { - "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", - "type": "number" - }, - "data": { - "type": "object", - "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, - "baseTokenBalanceChange": { - "description": "Change in base token balance (negative for decrease)", - "type": "number" - }, - "quoteTokenBalanceChange": { - "description": "Change in quote token balance (negative for decrease)", - "type": "number" - } - }, - "required": [ - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "fee", - "baseTokenBalanceChange", - "quoteTokenBalanceChange" - ] - } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, - "required": ["signature", "status"] + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] } } } @@ -1943,10 +7501,10 @@ } } }, - "/connectors/jupiter/router/execute-swap": { + "/connectors/uniswap/clmm/execute-swap": { "post": { - "tags": ["/connector/jupiter"], - "description": "Quote and execute a token swap on Jupiter in one step", + "tags": ["/connector/uniswap"], + "description": "Execute a swap on Uniswap V3 CLMM using SwapRouter02", "requestBody": { "content": { "application/json": { @@ -1954,60 +7512,36 @@ "type": "object", "properties": { "walletAddress": { - "description": "Solana wallet address that will execute the swap", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "type": "string" + "description": "Wallet address that will execute the swap", + "default": "", + "type": "string", + "example": "" }, "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], + "description": "The blockchain network to use", + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], "type": "string" }, "baseToken": { - "description": "Solana token symbol or address to determine swap direction", - "type": "string", - "example": "SOL" - }, - "quoteToken": { - "description": "The other Solana token symbol or address in the pair", + "description": "Token to determine swap direction", "type": "string", - "example": "USDC" - }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.1 }, - "side": { - "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", - "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string" - }, - "slippagePct": { - "minimum": 0, - "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 1, - "type": "number" - }, - "restrictIntermediateTokens": { - "description": "Restrict routing through highly liquid intermediate tokens only for better price and stability", - "default": true, - "type": "boolean" - }, - "onlyDirectRoutes": { - "description": "Restrict routing to only go through 1 market", - "default": false, - "type": "boolean" - }, - "priorityLevel": { - "description": "Priority level for Solana transaction processing", - "enum": ["medium", "high", "veryHigh"], - "default": "veryHigh", + "example": "WETH" + }, + "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.001 }, + "side": { + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", + "enum": ["BUY", "SELL"], "type": "string" }, - "maxLamports": { - "description": "Maximum priority fee in lamports for Solana transaction", - "default": 1000000, - "type": "number" + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 1 } }, "required": ["baseToken", "quoteToken", "amount", "side"] @@ -2065,76 +7599,60 @@ } } }, - "/connectors/meteora/clmm/fetch-pools": { - "get": { - "tags": ["/connector/meteora"], - "description": "Fetch info about Meteora pools", - "parameters": [ - { - "schema": { "minimum": 1, "default": 10, "type": "number" }, - "example": 10, - "in": "query", - "name": "limit", - "required": false, - "description": "Maximum number of pools to return" - }, - { - "schema": { "type": "string" }, - "example": "SOL", - "in": "query", - "name": "tokenA", - "required": false, - "description": "First token symbol or address" - }, - { - "schema": { "type": "string" }, - "example": "USDC", - "in": "query", - "name": "tokenB", - "required": false, - "description": "Second token symbol or address" + "/connectors/uniswap/clmm/open-position": { + "post": { + "tags": ["/connector/uniswap"], + "description": "Open a new liquidity position in a Uniswap V3 pool", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "" }, + "lowerPrice": { "type": "number", "example": 1000 }, + "upperPrice": { "type": "number", "example": 4000 }, + "poolAddress": { "type": "string", "example": "0xd0b53d9277642d899df5c87a3966a349a798f224" }, + "baseTokenAmount": { "type": "number", "example": 0.001 }, + "quoteTokenAmount": { "type": "number", "example": 3 }, + "slippagePct": { "type": "number", "example": 1 } + }, + "required": ["lowerPrice", "upperPrice", "poolAddress"] + } + } }, - { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" - } - ], + "required": true + }, "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "binStep": { "type": "number" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "activeBinId": { "type": "number" } - }, - "required": [ - "address", - "baseTokenAddress", - "quoteTokenAddress", - "binStep", - "feePct", - "price", - "baseTokenAmount", - "quoteTokenAmount", - "activeBinId" - ], - "title": "PoolInfo" - } + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": [ + "fee", + "positionAddress", + "positionRent", + "baseTokenAmountAdded", + "quoteTokenAmountAdded" + ] + } + }, + "required": ["signature", "status"] } } } @@ -2142,27 +7660,50 @@ } } }, - "/connectors/meteora/clmm/pool-info": { - "get": { - "tags": ["/connector/meteora"], - "description": "Get pool information for a Meteora pool", - "parameters": [ - { - "schema": { "type": "string" }, - "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", - "in": "query", - "name": "poolAddress", - "required": true, - "description": "Meteora DLMM pool address" + "/connectors/uniswap/clmm/add-liquidity": { + "post": { + "tags": ["/connector/uniswap"], + "description": "Add liquidity to an existing Uniswap V3 position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The EVM network to use", + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "type": "string" + }, + "walletAddress": { + "description": "Wallet address that will add liquidity", + "default": "", + "type": "string" + }, + "positionAddress": { "description": "NFT token ID of the position", "type": "string" }, + "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number" + }, + "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, + "maxGas": { + "description": "Maximum gas limit for the transaction", + "type": "number", + "example": 300000 + } + }, + "required": ["positionAddress", "baseTokenAmount", "quoteTokenAmount"] + } + } }, - { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" - } - ], + "required": true + }, "responses": { "200": { "description": "Default Response", @@ -2171,48 +7712,69 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "binStep": { "type": "number" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "activeBinId": { "type": "number" }, - "dynamicFeePct": { "type": "number" }, - "minBinId": { "type": "number" }, - "maxBinId": { "type": "number" }, - "bins": { - "type": "array", - "items": { - "type": "object", - "properties": { - "binId": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } - }, - "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"], - "title": "BinLiquidity" - } + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" }, + "newPositionAddress": { "type": "string" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/uniswap/clmm/remove-liquidity": { + "post": { + "tags": ["/connector/uniswap"], + "description": "Remove liquidity from a Uniswap V3 position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "" }, + "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" }, + "percentageToRemove": { "type": "number", "minimum": 0, "maximum": 100, "example": 50 } + }, + "required": ["positionAddress", "percentageToRemove"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } }, - "required": [ - "address", - "baseTokenAddress", - "quoteTokenAddress", - "binStep", - "feePct", - "price", - "baseTokenAmount", - "quoteTokenAmount", - "activeBinId", - "dynamicFeePct", - "minBinId", - "maxBinId", - "bins" - ] + "required": ["signature", "status"] } } } @@ -2220,76 +7782,47 @@ } } }, - "/connectors/meteora/clmm/positions-owned": { - "get": { - "tags": ["/connector/meteora"], - "description": "Retrieve a list of positions owned by a user's wallet in a specific Meteora pool", - "parameters": [ - { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" - }, - { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "in": "query", - "name": "walletAddress", - "required": false, - "description": "Solana wallet address to check for positions" + "/connectors/uniswap/clmm/collect-fees": { + "post": { + "tags": ["/connector/uniswap"], + "description": "Collect fees from a Uniswap V3 position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "" }, + "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" } + }, + "required": ["positionAddress"] + } + } }, - { - "schema": { "type": "string" }, - "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", - "in": "query", - "name": "poolAddress", - "required": true, - "description": "Meteora DLMM pool address" - } - ], + "required": true + }, "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } - }, - "required": [ - "address", - "poolAddress", - "baseTokenAddress", - "quoteTokenAddress", - "baseTokenAmount", - "quoteTokenAmount", - "baseFeeAmount", - "quoteFeeAmount", - "lowerBinId", - "upperBinId", - "lowerPrice", - "upperPrice", - "price" - ], - "title": "PositionInfo" - } + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] + } + }, + "required": ["signature", "status"] } } } @@ -2297,35 +7830,26 @@ } } }, - "/connectors/meteora/clmm/position-info": { - "get": { - "tags": ["/connector/meteora"], - "description": "Get details for a specific Meteora position", - "parameters": [ - { - "schema": { "type": "string" }, - "example": "", - "in": "query", - "name": "positionAddress", - "required": true, - "description": "Position NFT address" - }, - { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" + "/connectors/uniswap/clmm/close-position": { + "post": { + "tags": ["/connector/uniswap"], + "description": "Close a Uniswap V3 position by removing all liquidity and collecting fees", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string" }, + "walletAddress": { "type": "string" }, + "positionAddress": { "type": "string" } + }, + "required": ["positionAddress"] + } + } }, - { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "in": "query", - "name": "walletAddress", - "required": false, - "description": "Solana wallet address" - } - ], + "required": true + }, "responses": { "200": { "description": "Default Response", @@ -2334,35 +7858,29 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": [ + "fee", + "positionRentRefunded", + "baseTokenAmountRemoved", + "quoteTokenAmountRemoved", + "baseFeeAmountCollected", + "quoteFeeAmountCollected" + ] + } }, - "required": [ - "address", - "poolAddress", - "baseTokenAddress", - "quoteTokenAddress", - "baseTokenAmount", - "quoteTokenAmount", - "baseFeeAmount", - "quoteFeeAmount", - "lowerBinId", - "upperBinId", - "lowerPrice", - "upperPrice", - "price" - ] + "required": ["signature", "status"] } } } @@ -2370,65 +7888,74 @@ } } }, - "/connectors/meteora/clmm/quote-swap": { + "/connectors/0x/router/quote-swap": { "get": { - "tags": ["/connector/meteora"], - "description": "Get swap quote for Meteora CLMM", + "tags": ["/connector/0x"], + "description": "Get a swap quote from 0x. Use indicativePrice=true for price discovery only, or false/undefined for executable quotes", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "mainnet", "optimism", "polygon"], + "type": "string" + }, "in": "query", "name": "network", "required": false, - "description": "Solana network to use" - }, - { - "schema": { "type": "string" }, - "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", - "in": "query", - "name": "poolAddress", - "required": false, - "description": "Meteora DLMM pool address (optional - can be looked up from baseToken and quoteToken)" + "description": "The EVM network to use" }, { "schema": { "type": "string" }, - "example": "SOL", + "example": "WETH", "in": "query", "name": "baseToken", "required": true, - "description": "Token to determine swap direction" + "description": "First token in the trading pair" }, { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", - "required": false, - "description": "The other token in the pair (optional - required if poolAddress not provided)" + "required": true, + "description": "Second token in the trading pair" }, { "schema": { "type": "number" }, - "example": 0.01, + "example": 1, "in": "query", "name": "amount", "required": true, - "description": "Amount to swap" + "description": "Amount of base token to trade" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, - "example": "SELL", + "schema": { "enum": ["BUY", "SELL"], "type": "string" }, "in": "query", "name": "side", "required": true, - "description": "Trade direction" + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" + }, + { + "schema": { "default": true, "type": "boolean" }, + "in": "query", + "name": "indicativePrice", + "required": false, + "description": "If true, returns indicative pricing only (no commitment). If false, returns firm quote ready for execution" + }, + { + "schema": { "type": "string" }, + "in": "query", + "name": "takerAddress", + "required": false, + "description": "Ethereum wallet address that will execute the swap (optional for quotes)" } ], "responses": { @@ -2439,27 +7966,43 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "minAmountOut": { + "description": "Minimum amount of tokenOut that will be accepted", + "type": "number" + }, + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "expirationTime": { + "description": "Unix timestamp when this quote expires (only for firm quotes)", + "type": "number" + }, + "gasEstimate": { "description": "Estimated gas required for the swap", "type": "string" }, + "sources": { "description": "Liquidity sources used for this quote", "type": "array", "items": {} }, + "allowanceTarget": { + "description": "Contract address that needs token approval", + "type": "string" + }, + "to": { "description": "Contract address to send transaction to", "type": "string" }, + "data": { "description": "Encoded transaction data", "type": "string" }, + "value": { "description": "ETH value to send with transaction", "type": "string" } }, "required": [ - "poolAddress", + "quoteId", "tokenIn", "tokenOut", "amountIn", "amountOut", "price", + "priceImpactPct", "minAmountOut", "maxAmountIn", - "priceImpactPct" + "gasEstimate" ] } } @@ -2468,57 +8011,41 @@ } } }, - "/connectors/meteora/clmm/execute-swap": { + "/connectors/0x/router/execute-quote": { "post": { - "tags": ["/connector/meteora"], - "description": "Execute a token swap on Meteora DLMM", + "tags": ["/connector/0x"], + "description": "Execute a previously fetched quote from 0x", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, "walletAddress": { - "description": "Solana wallet address that will execute the swap", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "type": "string", - "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" - }, - "poolAddress": { - "description": "Meteora DLMM pool address (optional - can be looked up from baseToken and quoteToken)", - "type": "string", - "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3" + "description": "Wallet address that will execute the swap", + "default": "", + "type": "string" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, - "quoteToken": { - "description": "Quote token symbol or address (optional - required if poolAddress not provided)", + "network": { + "description": "The blockchain network to use", + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "mainnet", "optimism", "polygon"], "type": "string", - "example": "USDC" + "example": "arbitrum" }, - "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, - "side": { - "description": "Trade direction", - "enum": ["BUY", "SELL"], - "default": "SELL", + "quoteId": { + "description": "ID of the quote to execute", "type": "string", - "example": "SELL" + "example": "123e4567-e89b-12d3-a456-426614174000" }, - "slippagePct": { - "minimum": 0, - "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 1, + "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, + "maxGas": { + "description": "Maximum gas limit for the transaction", "type": "number", - "example": 1 + "example": 1000000 } }, - "required": ["baseToken", "amount", "side"] + "required": ["quoteId"] } } }, @@ -2532,18 +8059,27 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } }, "required": [ "tokenIn", @@ -2564,179 +8100,60 @@ } } }, - "/connectors/meteora/clmm/open-position": { + "/connectors/0x/router/execute-swap": { "post": { - "tags": ["/connector/meteora"], - "description": "Open a new Meteora position", + "tags": ["/connector/0x"], + "description": "Quote and execute a token swap on 0x in one step", "requestBody": { "content": { "application/json": { "schema": { "type": "object", - "allOf": [ - { - "type": "object", - "properties": { - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "poolAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } - }, - "required": ["lowerPrice", "upperPrice", "poolAddress"] + "properties": { + "walletAddress": { + "description": "Wallet address that will execute the swap", + "default": "", + "type": "string", + "example": "" }, - { - "type": "object", - "properties": { - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "walletAddress": { - "description": "Solana wallet address that will open the position", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "examples": ["82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5"], - "type": "string" - }, - "lowerPrice": { - "description": "Lower price bound for the position", - "examples": [100], - "type": "number" - }, - "upperPrice": { - "description": "Upper price bound for the position", - "examples": [300], - "type": "number" - }, - "poolAddress": { - "description": "Meteora DLMM pool address", - "examples": ["2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3"], - "type": "string" - }, - "baseTokenAmount": { - "description": "Amount of base token to deposit", - "examples": [0.01], - "type": "number" - }, - "quoteTokenAmount": { - "description": "Amount of quote token to deposit", - "examples": [2], - "type": "number" - }, - "slippagePct": { - "minimum": 0, - "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 1, - "examples": [1], - "type": "number" - }, - "strategyType": { - "description": "Strategy type for the position", - "examples": [0], - "enum": [0, 1, 2, 3, 4, 5], - "type": "number" - } - }, - "required": ["lowerPrice", "upperPrice", "poolAddress"] - } - ] - } - } - } - }, - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "data": { - "type": "object", - "properties": { - "fee": { "type": "number" }, - "positionAddress": { "type": "string" }, - "positionRent": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } - }, - "required": [ - "fee", - "positionAddress", - "positionRent", - "baseTokenAmountAdded", - "quoteTokenAmountAdded" - ] - } + "network": { + "description": "The blockchain network to use", + "default": "mainnet", + "enum": ["arbitrum", "avalanche", "base", "bsc", "mainnet", "optimism", "polygon"], + "type": "string", + "example": "arbitrum" }, - "required": ["signature", "status"] - } - } - } - } - } - } - }, - "/connectors/meteora/clmm/add-liquidity": { - "post": { - "tags": ["/connector/meteora"], - "description": "Add liquidity to a Meteora position", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "allOf": [ - { - "type": "object", - "properties": { - "positionAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } - }, - "required": ["positionAddress", "baseTokenAmount", "quoteTokenAmount"] + "baseToken": { + "description": "Token to determine swap direction", + "type": "string", + "example": "WETH" }, - { - "type": "object", - "properties": { - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "walletAddress": { - "description": "Solana wallet address that will add liquidity", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "examples": ["82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5"], - "type": "string" - }, - "slippagePct": { - "minimum": 0, - "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 1, - "examples": [1], - "type": "number" - }, - "strategyType": { - "description": "Strategy type for the position", - "examples": [0], - "enum": [0, 1, 2, 3, 4, 5], - "type": "number" - } - } + "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 1 }, + "side": { + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", + "enum": ["BUY", "SELL"], + "type": "string" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "type": "number", + "example": 1 + }, + "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, + "maxGas": { + "description": "Maximum gas limit for the transaction", + "type": "number", + "example": 300000 } - ] + }, + "required": ["baseToken", "quoteToken", "amount", "side"] } } - } + }, + "required": true }, "responses": { "200": { @@ -2746,16 +8163,37 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } }, - "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] } }, "required": ["signature", "status"] @@ -2766,46 +8204,64 @@ } } }, - "/connectors/meteora/clmm/remove-liquidity": { - "post": { - "tags": ["/connector/meteora"], - "description": "Remove liquidity from a Meteora position", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "allOf": [ - { - "type": "object", - "properties": { - "positionAddress": { "type": "string" }, - "percentageToRemove": { "minimum": 0, "maximum": 100, "type": "number" } - }, - "required": ["positionAddress", "percentageToRemove"] - }, - { - "type": "object", - "properties": { - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "walletAddress": { - "description": "Solana wallet address that will remove liquidity", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "examples": ["82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5"], - "type": "string" - } - } - } - ] - } - } + "/connectors/pancakeswap/router/quote-swap": { + "get": { + "tags": ["/connector/pancakeswap"], + "description": "Get an executable swap quote from Pancakeswap Universal Router", + "parameters": [ + { + "schema": { "default": "mainnet", "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "The EVM network to use" + }, + { + "schema": { "type": "string" }, + "example": "USDT", + "in": "query", + "name": "baseToken", + "required": true, + "description": "First token in the trading pair" + }, + { + "schema": { "type": "string" }, + "example": "WBNB", + "in": "query", + "name": "quoteToken", + "required": true, + "description": "Second token in the trading pair" + }, + { + "schema": { "type": "number" }, + "example": 10, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount of base token to trade" + }, + { + "schema": { "enum": ["BUY", "SELL"], "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + }, + { + "schema": { "default": "", "type": "string" }, + "in": "query", + "name": "walletAddress", + "required": false, + "description": "Wallet address for more accurate quotes (optional)" } - }, + ], "responses": { "200": { "description": "Default Response", @@ -2814,19 +8270,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "data": { - "type": "object", - "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } - }, - "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] - } + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "minAmountOut": { + "description": "Minimum amount of tokenOut that will be accepted", + "type": "number" + }, + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "routePath": { "description": "Human-readable route path", "type": "string" } }, - "required": ["signature", "status"] + "required": [ + "quoteId", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "priceImpactPct", + "minAmountOut", + "maxAmountIn" + ] } } } @@ -2834,42 +8302,39 @@ } } }, - "/connectors/meteora/clmm/collect-fees": { + "/connectors/pancakeswap/router/execute-quote": { "post": { - "tags": ["/connector/meteora"], - "description": "Collect fees from a Meteora position", + "tags": ["/connector/pancakeswap"], + "description": "Execute a previously fetched quote from Pancakeswap Universal Router", "requestBody": { "content": { "application/json": { "schema": { "type": "object", - "allOf": [ - { - "type": "object", - "properties": { "positionAddress": { "type": "string" } }, - "required": ["positionAddress"] + "properties": { + "walletAddress": { + "description": "Wallet address that will execute the swap", + "default": "", + "type": "string", + "example": "" }, - { - "type": "object", - "properties": { - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "walletAddress": { - "description": "Solana wallet address that will collect fees", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "examples": ["82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5"], - "type": "string" - } - } + "network": { + "description": "The blockchain network to use", + "default": "mainnet", + "enum": ["arbitrum", "base", "bsc", "mainnet"], + "type": "string" + }, + "quoteId": { + "description": "ID of the quote to execute", + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" } - ] + }, + "required": ["quoteId"] } } - } + }, + "required": true }, "responses": { "200": { @@ -2879,16 +8344,37 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } }, - "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] } }, "required": ["signature", "status"] @@ -2899,42 +8385,54 @@ } } }, - "/connectors/meteora/clmm/close-position": { + "/connectors/pancakeswap/router/execute-swap": { "post": { - "tags": ["/connector/meteora"], - "description": "Close a Meteora position", + "tags": ["/connector/pancakeswap"], + "description": "Quote and execute a token swap on Pancakeswap Universal Router in one step", "requestBody": { "content": { "application/json": { "schema": { "type": "object", - "allOf": [ - { - "type": "object", - "properties": { "positionAddress": { "type": "string" } }, - "required": ["positionAddress"] + "properties": { + "walletAddress": { + "description": "Wallet address that will execute the swap", + "default": "", + "type": "string", + "example": "" }, - { - "type": "object", - "properties": { - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "walletAddress": { - "description": "Solana wallet address that will close the position", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "examples": ["82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5"], - "type": "string" - } - } + "network": { + "description": "The blockchain network to use", + "default": "mainnet", + "enum": ["arbitrum", "base", "bsc", "mainnet"], + "type": "string" + }, + "baseToken": { + "description": "Token to determine swap direction", + "type": "string", + "example": "USDT" + }, + "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "WBNB" }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 10 }, + "side": { + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", + "enum": ["BUY", "SELL"], + "type": "string" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 1 } - ] + }, + "required": ["baseToken", "quoteToken", "amount", "side"] } } - } + }, + "required": true }, "responses": { "200": { @@ -2944,25 +8442,36 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionRentRefunded": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } }, "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", "fee", - "positionRentRefunded", - "baseTokenAmountRemoved", - "quoteTokenAmountRemoved", - "baseFeeAmountCollected", - "quoteFeeAmountCollected" + "baseTokenBalanceChange", + "quoteTokenBalanceChange" ] } }, @@ -2974,25 +8483,25 @@ } } }, - "/connectors/raydium/amm/pool-info": { + "/connectors/pancakeswap/amm/pool-info": { "get": { - "tags": ["/connector/raydium"], - "description": "Get AMM pool information from Raydium", + "tags": ["/connector/pancakeswap"], + "description": "Get AMM pool information from Pancakeswap V2", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { "default": "mainnet", "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, "in": "query", "name": "network", "required": false, - "description": "Solana network to use" + "description": "The EVM network to use" }, { "schema": { "type": "string" }, - "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", + "example": "0x88A43bbDF9D098eEC7bCEda4e2494615dfD9bB9C", "in": "query", "name": "poolAddress", "required": true, - "description": "Raydium AMM pool address" + "description": "Pancakeswap V2 pool address" } ], "responses": { @@ -3027,33 +8536,22 @@ } } }, - "/connectors/raydium/amm/position-info": { + "/connectors/pancakeswap/amm/position-info": { "get": { - "tags": ["/connector/raydium"], - "description": "Get info about a Raydium AMM position", + "tags": ["/connector/pancakeswap"], + "description": "Get position information for a Pancakeswap V2 pool", "parameters": [ - { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" - }, + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { "schema": { "type": "string" }, - "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", - "in": "query", - "name": "poolAddress", - "required": true, - "description": "Raydium AMM pool address" - }, - { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "example": "", "in": "query", "name": "walletAddress", - "required": false, - "description": "Solana wallet address" - } + "required": false + }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } ], "responses": { "200": { @@ -3089,50 +8587,24 @@ } } }, - "/connectors/raydium/amm/quote-liquidity": { + "/connectors/pancakeswap/amm/quote-swap": { "get": { - "tags": ["/connector/raydium"], - "description": "Quote amounts for a new Raydium AMM liquidity position", + "tags": ["/connector/pancakeswap"], + "description": "Get swap quote for Pancakeswap V2 AMM", "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": false }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, + { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" - }, - { - "schema": { "type": "string" }, - "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", - "in": "query", - "name": "poolAddress", - "required": true, - "description": "Raydium AMM pool address" - }, - { - "schema": { "type": "number" }, - "example": 0.01, - "in": "query", - "name": "baseTokenAmount", - "required": true, - "description": "Amount of base token to add" - }, - { - "schema": { "type": "number" }, - "example": 2, + "schema": { "type": "string", "enum": ["BUY", "SELL"] }, + "example": "SELL", "in": "query", - "name": "quoteTokenAmount", - "required": true, - "description": "Amount of quote token to add" + "name": "side", + "required": true }, - { - "schema": { "minimum": 0, "maximum": 100, "default": 10, "type": "number" }, - "example": 10, - "in": "query", - "name": "slippagePct", - "required": false, - "description": "Maximum acceptable slippage percentage" - } + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } ], "responses": { "200": { @@ -3142,91 +8614,59 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, "required": [ - "baseLimited", - "baseTokenAmount", - "quoteTokenAmount", - "baseTokenAmountMax", - "quoteTokenAmountMax" + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" ] } } } - }, - "500": { - "description": "Default Response", - "content": { - "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } - } } } } }, - "/connectors/raydium/amm/quote-swap": { + "/connectors/pancakeswap/amm/quote-liquidity": { "get": { - "tags": ["/connector/raydium"], - "description": "Get swap quote for Raydium AMM", + "tags": ["/connector/pancakeswap"], + "description": "Get liquidity quote for a Pancakeswap V2 pool", "parameters": [ - { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" - }, - { - "schema": { "type": "string" }, - "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", - "in": "query", - "name": "poolAddress", - "required": false, - "description": "AMM pool address (optional - can be looked up from baseToken and quoteToken)" - }, - { - "schema": { "type": "string" }, - "example": "SOL", - "in": "query", - "name": "baseToken", - "required": true, - "description": "Token to determine swap direction" - }, - { - "schema": { "type": "string" }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": false, - "description": "The other token in the pair (optional - required if poolAddress not provided)" - }, + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, { "schema": { "type": "number" }, - "example": 0.01, + "example": 0.001, "in": "query", - "name": "amount", - "required": true, - "description": "Amount to swap" + "name": "baseTokenAmount", + "required": true }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { "type": "number" }, + "example": 2.5, "in": "query", - "name": "side", - "required": true, - "description": "Trade direction" + "name": "quoteTokenAmount", + "required": true }, - { - "schema": { "minimum": 0, "maximum": 100, "default": 10, "type": "number" }, - "example": 10, - "in": "query", - "name": "slippagePct", - "required": false, - "description": "Maximum acceptable slippage percentage" - } + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } ], "responses": { "200": { @@ -3236,27 +8676,18 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" } }, "required": [ - "poolAddress", - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "price", - "minAmountOut", - "maxAmountIn", - "priceImpactPct" + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" ] } } @@ -3265,24 +8696,43 @@ } } }, - "/connectors/raydium/amm/execute-swap": { + "/connectors/pancakeswap/amm/execute-swap": { "post": { - "tags": ["/connector/raydium"], - "description": "Execute a swap on Raydium AMM or CPMM", + "tags": ["/connector/pancakeswap"], + "description": "Execute a swap on Pancakeswap V2 AMM using Router02", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "walletAddress": { "type": "string", "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, - "network": { "type": "string", "default": "mainnet-beta" }, - "poolAddress": { "type": "string", "example": "" }, - "baseToken": { "type": "string", "example": "SOL" }, - "quoteToken": { "type": "string", "example": "USDC" }, - "amount": { "type": "number", "example": 0.01 }, - "side": { "type": "string", "example": "SELL" }, - "slippagePct": { "type": "number", "example": 1 } + "walletAddress": { + "description": "Wallet address that will execute the swap", + "default": "", + "type": "string" + }, + "network": { + "description": "The EVM network to use", + "default": "mainnet", + "enum": ["arbitrum", "base", "bsc", "mainnet"], + "type": "string" + }, + "poolAddress": { + "description": "Pool address (optional - can be looked up from tokens)", + "default": "", + "type": "string" + }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "USDT" }, + "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "WBNB" }, + "amount": { "description": "Amount to swap", "type": "number", "example": 10 }, + "side": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number" + } }, "required": ["baseToken", "amount", "side"] } @@ -3298,18 +8748,27 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } }, "required": [ "tokenIn", @@ -3330,10 +8789,10 @@ } } }, - "/connectors/raydium/amm/add-liquidity": { + "/connectors/pancakeswap/amm/add-liquidity": { "post": { - "tags": ["/connector/raydium"], - "description": "Add liquidity to a Raydium AMM/CPMM pool", + "tags": ["/connector/pancakeswap"], + "description": "Add liquidity to a Pancakeswap V2 pool", "requestBody": { "content": { "application/json": { @@ -3341,34 +8800,31 @@ "type": "object", "properties": { "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], + "description": "The EVM network to use", + "default": "mainnet", + "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, "walletAddress": { - "description": "Solana wallet address", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "description": "Wallet address that will add liquidity", + "default": "", "type": "string" }, - "poolAddress": { - "description": "Raydium AMM pool address", - "type": "string", - "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" - }, - "baseTokenAmount": { - "description": "Amount of base token to add", - "type": "number", - "example": 0.01 - }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, + "poolAddress": { "description": "Address of the Pancakeswap V2 pool", "type": "string" }, + "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, "slippagePct": { "minimum": 0, "maximum": 100, "description": "Maximum acceptable slippage percentage", - "default": 10, + "default": 2, + "type": "number" + }, + "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, + "maxGas": { + "description": "Maximum gas limit for the transaction", "type": "number", - "example": 10 + "example": 300000 } }, "required": ["poolAddress", "baseTokenAmount", "quoteTokenAmount"] @@ -3405,10 +8861,10 @@ } } }, - "/connectors/raydium/amm/remove-liquidity": { + "/connectors/pancakeswap/amm/remove-liquidity": { "post": { - "tags": ["/connector/raydium"], - "description": "Remove liquidity from a Raydium AMM/CPMM pool", + "tags": ["/connector/pancakeswap"], + "description": "Remove liquidity from a Pancakeswap V2 pool", "requestBody": { "content": { "application/json": { @@ -3416,27 +8872,28 @@ "type": "object", "properties": { "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], + "description": "The EVM network to use", + "default": "mainnet", + "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, "walletAddress": { - "description": "Solana wallet address", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "description": "Wallet address that will remove liquidity", + "default": "", "type": "string" }, - "poolAddress": { - "description": "Raydium AMM pool address", - "type": "string", - "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" - }, + "poolAddress": { "description": "Address of the Pancakeswap V2 pool", "type": "string" }, "percentageToRemove": { "minimum": 0, "maximum": 100, "description": "Percentage of liquidity to remove", + "type": "number" + }, + "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, + "maxGas": { + "description": "Maximum gas limit for the transaction", "type": "number", - "example": 100 + "example": 300000 } }, "required": ["poolAddress", "percentageToRemove"] @@ -3473,25 +8930,26 @@ } } }, - "/connectors/raydium/clmm/pool-info": { + "/connectors/pancakeswap/clmm/pool-info": { "get": { - "tags": ["/connector/raydium"], - "description": "Get CLMM pool information from Raydium", + "tags": ["/connector/pancakeswap"], + "description": "Get CLMM pool information from Pancakeswap V3", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { "default": "bsc", "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, + "example": "bsc", "in": "query", "name": "network", "required": false, - "description": "Solana network to use" + "description": "The EVM network to use" }, { "schema": { "type": "string" }, - "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", + "example": "0x172fcd41e0913e95784454622d1c3724f546f849", "in": "query", "name": "poolAddress", "required": true, - "description": "Raydium CLMM pool address" + "description": "Pancakeswap V3 pool address" } ], "responses": { @@ -3516,7 +8974,6 @@ "address", "baseTokenAddress", "quoteTokenAddress", - "binStep", "feePct", "price", "baseTokenAmount", @@ -3530,32 +8987,91 @@ } } }, - "/connectors/raydium/clmm/positions-owned": { + "/connectors/pancakeswap/clmm/position-info": { "get": { - "tags": ["/connector/raydium"], - "description": "Retrieve a list of positions owned by a user's wallet in a specific Raydium CLMM pool", + "tags": ["/connector/pancakeswap"], + "description": "Get position information for a Pancakeswap V3 position", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { "type": "string", "default": "bsc" }, + "example": "bsc", "in": "query", "name": "network", - "required": false, - "description": "Solana network to use" + "required": false }, { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "schema": { "type": "string" }, + "example": "1234", "in": "query", - "name": "walletAddress", - "required": false, - "description": "Solana wallet address to check for positions" + "name": "positionAddress", + "required": true, + "description": "Position NFT token ID" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/pancakeswap/clmm/positions-owned": { + "get": { + "tags": ["/connector/pancakeswap"], + "description": "Get all Pancakeswap V3 positions owned by a wallet", + "parameters": [ + { + "schema": { "default": "bsc", "type": "string" }, + "example": "bsc", + "in": "query", + "name": "network", + "required": false }, { "schema": { "type": "string" }, - "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", + "example": "", "in": "query", - "name": "poolAddress", - "required": true, - "description": "Raydium CLMM pool address (required for Raydium)" + "name": "walletAddress", + "required": true } ], "responses": { @@ -3580,7 +9096,9 @@ "upperBinId": { "type": "number" }, "lowerPrice": { "type": "number" }, "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } }, "required": [ "address", @@ -3605,137 +9123,46 @@ } } }, - "/connectors/raydium/clmm/position-info": { - "get": { - "tags": ["/connector/raydium"], - "description": "Get info about a Raydium CLMM position", - "parameters": [ - { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" - }, - { - "schema": { "type": "string" }, - "example": "", - "in": "query", - "name": "positionAddress", - "required": true, - "description": "Position NFT address" - }, - { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "in": "query", - "name": "walletAddress", - "required": false, - "description": "Solana wallet address" - } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } - }, - "required": [ - "address", - "poolAddress", - "baseTokenAddress", - "quoteTokenAddress", - "baseTokenAmount", - "quoteTokenAmount", - "baseFeeAmount", - "quoteFeeAmount", - "lowerBinId", - "upperBinId", - "lowerPrice", - "upperPrice", - "price" - ] - } - } - } - } - } - } - }, - "/connectors/raydium/clmm/quote-position": { + "/connectors/pancakeswap/clmm/quote-position": { "get": { - "tags": ["/connector/raydium"], - "description": "Quote amounts for a new Raydium CLMM position", + "tags": ["/connector/pancakeswap"], + "description": "Get a quote for opening a position on Pancakeswap V3", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { "type": "string", "default": "bsc" }, + "example": "bsc", "in": "query", "name": "network", - "required": false, - "description": "Solana network to use" - }, - { - "schema": { "type": "number" }, - "example": 100, - "in": "query", - "name": "lowerPrice", - "required": true, - "description": "Lower price bound for the position" - }, - { - "schema": { "type": "number" }, - "example": 300, - "in": "query", - "name": "upperPrice", - "required": true, - "description": "Upper price bound for the position" + "required": false }, + { "schema": { "type": "number" }, "example": 0.0008, "in": "query", "name": "lowerPrice", "required": true }, + { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "upperPrice", "required": true }, { - "schema": { "type": "string" }, - "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", + "schema": { "type": "string", "default": "0x172fcd41e0913e95784454622d1c3724f546f849" }, + "example": "0x172fcd41e0913e95784454622d1c3724f546f849", "in": "query", "name": "poolAddress", - "required": true, - "description": "Raydium CLMM pool address" + "required": true }, { "schema": { "type": "number" }, - "example": 0.01, + "example": 10, "in": "query", "name": "baseTokenAmount", - "required": false, - "description": "Amount of base token to deposit" + "required": false }, { "schema": { "type": "number" }, - "example": 2, + "example": 0.01, "in": "query", "name": "quoteTokenAmount", - "required": false, - "description": "Amount of quote token to deposit" + "required": false }, { - "schema": { "minimum": 0, "maximum": 100, "default": 10, "type": "number" }, - "example": 10, + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, "in": "query", "name": "slippagePct", - "required": false, - "description": "Maximum acceptable slippage percentage" + "required": false } ], "responses": { @@ -3767,65 +9194,36 @@ } } }, - "/connectors/raydium/clmm/quote-swap": { + "/connectors/pancakeswap/clmm/quote-swap": { "get": { - "tags": ["/connector/raydium"], - "description": "Get swap quote for Raydium CLMM", + "tags": ["/connector/pancakeswap"], + "description": "Get swap quote for Pancakeswap V3 CLMM", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { "type": "string", "default": "bsc" }, + "example": "bsc", "in": "query", "name": "network", - "required": false, - "description": "Solana network to use" + "required": false }, { "schema": { "type": "string" }, - "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", "in": "query", "name": "poolAddress", "required": false, - "description": "CLMM pool address (optional - can be looked up from tokens)" - }, - { - "schema": { "type": "string" }, - "example": "SOL", - "in": "query", - "name": "baseToken", - "required": true, - "description": "Token to determine swap direction" - }, - { - "schema": { "type": "string" }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": false, - "description": "The other token in the pair" - }, - { - "schema": { "type": "number" }, - "example": 0.01, - "in": "query", - "name": "amount", - "required": true, - "description": "Amount to swap" + "description": "Pool address (optional - can be looked up from baseToken and quoteToken)" }, + { "schema": { "type": "string" }, "example": "USDT", "in": "query", "name": "baseToken", "required": true }, + { "schema": { "type": "string" }, "example": "WBNB", "in": "query", "name": "quoteToken", "required": false }, + { "schema": { "type": "number" }, "example": 10, "in": "query", "name": "amount", "required": true }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { "type": "string", "enum": ["BUY", "SELL"] }, + "example": "SELL", "in": "query", "name": "side", - "required": true, - "description": "Trade direction" + "required": true }, - { - "schema": { "minimum": 0, "maximum": 100, "default": 10, "type": "number" }, - "example": 10, - "in": "query", - "name": "slippagePct", - "required": false, - "description": "Maximum acceptable slippage percentage" - } + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } ], "responses": { "200": { @@ -3864,10 +9262,10 @@ } } }, - "/connectors/raydium/clmm/execute-swap": { + "/connectors/pancakeswap/clmm/execute-swap": { "post": { - "tags": ["/connector/raydium"], - "description": "Execute a swap on Raydium CLMM", + "tags": ["/connector/pancakeswap"], + "description": "Execute a swap on Pancakeswap V3 CLMM using SwapRouter02", "requestBody": { "content": { "application/json": { @@ -3875,42 +9273,39 @@ "type": "object", "properties": { "walletAddress": { - "description": "Solana wallet address", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "description": "Wallet address that will execute the swap", + "default": "", "type": "string", - "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + "example": "" }, "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], + "description": "The blockchain network to use", + "default": "mainnet", + "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, - "poolAddress": { - "description": "CLMM pool address (optional)", + "baseToken": { + "description": "Token to determine swap direction", "type": "string", - "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv" + "example": "USDT" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, - "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, + "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "WBNB" }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 10 }, "side": { - "description": "Trade direction", + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string", - "example": "SELL" + "type": "string" }, "slippagePct": { "minimum": 0, "maximum": 100, "description": "Maximum acceptable slippage percentage", - "default": 10, + "default": 2, "type": "number", - "example": 10 + "example": 1 } }, - "required": ["baseToken", "amount", "side"] + "required": ["baseToken", "quoteToken", "amount", "side"] } } }, @@ -3924,18 +9319,27 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } }, "required": [ "tokenIn", @@ -3956,60 +9360,28 @@ } } }, - "/connectors/raydium/clmm/open-position": { + "/connectors/pancakeswap/clmm/open-position": { "post": { - "tags": ["/connector/raydium"], - "description": "Open a new Raydium CLMM position", + "tags": ["/connector/pancakeswap"], + "description": "Open a new liquidity position in a Pancakeswap V3 pool", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "walletAddress": { - "description": "Solana wallet address", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "type": "string" - }, - "lowerPrice": { - "description": "Lower price bound for the position", - "type": "number", - "example": 100 - }, - "upperPrice": { - "description": "Upper price bound for the position", - "type": "number", - "example": 300 - }, + "network": { "type": "string", "default": "bsc", "example": "bsc" }, + "walletAddress": { "type": "string", "example": "" }, + "lowerPrice": { "type": "number", "example": 0.0008 }, + "upperPrice": { "type": "number", "example": 0.001 }, "poolAddress": { - "description": "Raydium CLMM pool address", "type": "string", - "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv" - }, - "baseTokenAmount": { - "description": "Amount of base token to deposit", - "type": "number", - "example": 0.01 - }, - "quoteTokenAmount": { - "description": "Amount of quote token to deposit", - "type": "number", - "example": 2 + "default": "0x172fcd41e0913e95784454622d1c3724f546f849", + "example": "0x172fcd41e0913e95784454622d1c3724f546f849" }, - "slippagePct": { - "minimum": 0, - "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 10, - "type": "number", - "example": 10 - } + "baseTokenAmount": { "type": "number", "example": 10 }, + "quoteTokenAmount": { "type": "number", "example": 0.01 }, + "slippagePct": { "type": "number", "example": 1 } }, "required": ["lowerPrice", "upperPrice", "poolAddress"] } @@ -4053,10 +9425,10 @@ } } }, - "/connectors/raydium/clmm/add-liquidity": { + "/connectors/pancakeswap/clmm/add-liquidity": { "post": { - "tags": ["/connector/raydium"], - "description": "Add liquidity to existing Raydium CLMM position", + "tags": ["/connector/pancakeswap"], + "description": "Add liquidity to an existing Pancakeswap V3 position", "requestBody": { "content": { "application/json": { @@ -4064,37 +9436,85 @@ "type": "object", "properties": { "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" + "description": "The EVM network to use", + "default": "bsc", + "enum": ["arbitrum", "base", "bsc", "mainnet"], + "type": "string", + "example": "bsc" }, "walletAddress": { - "description": "Solana wallet address", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "description": "Wallet address that will add liquidity", + "default": "", "type": "string" }, - "positionAddress": { - "description": "Position NFT address", - "type": "string", - "example": "" - }, - "baseTokenAmount": { - "description": "Amount of base token to add", - "type": "number", - "example": 0.01 - }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, + "positionAddress": { "description": "NFT token ID of the position", "type": "string" }, + "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, "slippagePct": { "minimum": 0, "maximum": 100, "description": "Maximum acceptable slippage percentage", - "default": 10, + "default": 2, + "type": "number" + }, + "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, + "maxGas": { + "description": "Maximum gas limit for the transaction", "type": "number", - "example": 10 + "example": 300000 } }, - "required": ["positionAddress", "baseTokenAmount", "quoteTokenAmount"] + "required": ["positionAddress", "baseTokenAmount", "quoteTokenAmount"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" }, + "newPositionAddress": { "type": "string" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/pancakeswap/clmm/remove-liquidity": { + "post": { + "tags": ["/connector/pancakeswap"], + "description": "Remove liquidity from a Pancakeswap V3 position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string", "default": "bsc", "example": "bsc" }, + "walletAddress": { "type": "string", "example": "" }, + "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" }, + "percentageToRemove": { "type": "number", "minimum": 0, "maximum": 100, "example": 50 } + }, + "required": ["positionAddress", "percentageToRemove"] } } }, @@ -4114,10 +9534,10 @@ "type": "object", "properties": { "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, - "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } }, "required": ["signature", "status"] @@ -4128,41 +9548,21 @@ } } }, - "/connectors/raydium/clmm/remove-liquidity": { + "/connectors/pancakeswap/clmm/collect-fees": { "post": { - "tags": ["/connector/raydium"], - "description": "Remove liquidity from Raydium CLMM position", + "tags": ["/connector/pancakeswap"], + "description": "Collect fees from a Pancakeswap V3 position", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "walletAddress": { - "description": "Solana wallet address", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "type": "string" - }, - "positionAddress": { - "description": "Position NFT address to remove liquidity from", - "type": "string", - "example": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" - }, - "percentageToRemove": { - "minimum": 0, - "maximum": 100, - "description": "Percentage of liquidity to remove", - "type": "number", - "example": 100 - } + "network": { "type": "string", "default": "bsc", "example": "bsc" }, + "walletAddress": { "type": "string", "example": "" }, + "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" } }, - "required": ["positionAddress", "percentageToRemove"] + "required": ["positionAddress"] } } }, @@ -4182,10 +9582,10 @@ "type": "object", "properties": { "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, - "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } }, "required": ["signature", "status"] @@ -4196,18 +9596,18 @@ } } }, - "/connectors/raydium/clmm/collect-fees": { + "/connectors/pancakeswap/clmm/close-position": { "post": { - "tags": ["/connector/raydium"], - "description": "Collect fees from a Raydium CLMM position by removing 1% of liquidity", + "tags": ["/connector/pancakeswap"], + "description": "Close a Pancakeswap V3 position by removing all liquidity and collecting fees", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { "type": "string", "default": "mainnet-beta" }, - "walletAddress": { "type": "string", "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, + "network": { "type": "string" }, + "walletAddress": { "type": "string" }, "positionAddress": { "type": "string" } }, "required": ["positionAddress"] @@ -4230,10 +9630,20 @@ "type": "object", "properties": { "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, "baseFeeAmountCollected": { "type": "number" }, "quoteFeeAmountCollected": { "type": "number" } }, - "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] + "required": [ + "fee", + "positionRentRefunded", + "baseTokenAmountRemoved", + "quoteTokenAmountRemoved", + "baseFeeAmountCollected", + "quoteFeeAmountCollected" + ] } }, "required": ["signature", "status"] @@ -4244,70 +9654,200 @@ } } }, - "/connectors/raydium/clmm/close-position": { - "post": { - "tags": ["/connector/raydium"], - "description": "Close a Raydium CLMM position", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" + "/connectors/pancakeswap-sol/clmm/pool-info": { + "get": { + "tags": ["/connector/pancakeswap-sol"], + "description": "Get CLMM pool information from PancakeSwap Solana", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "PancakeSwap CLMM pool address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" } }, - "walletAddress": { - "description": "Solana wallet address", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "type": "string" + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount", + "activeBinId" + ] + } + } + } + } + } + } + }, + "/connectors/pancakeswap-sol/clmm/position-info": { + "get": { + "tags": ["/connector/pancakeswap-sol"], + "description": "Get CLMM position information from PancakeSwap Solana", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "", + "in": "query", + "name": "positionAddress", + "required": true, + "description": "Position NFT address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } }, - "positionAddress": { - "description": "Position NFT address to close", - "type": "string", - "example": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/pancakeswap-sol/clmm/positions-owned": { + "get": { + "tags": ["/connector/pancakeswap-sol"], + "description": "Retrieve all positions owned by a user's wallet across all PancakeSwap Solana CLMM pools", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "", + "in": "query", + "name": "walletAddress", + "required": true, + "description": "Solana wallet address to check for positions" + }, + { + "schema": { "type": "string" }, + "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", + "in": "query", + "name": "poolAddress", + "required": false, + "description": "Optional pool address to filter positions by specific pool" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] } - }, - "required": ["positionAddress"] - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "data": { - "type": "object", - "properties": { - "fee": { "type": "number" }, - "positionRentRefunded": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } - }, - "required": [ - "fee", - "positionRentRefunded", - "baseTokenAmountRemoved", - "quoteTokenAmountRemoved", - "baseFeeAmountCollected", - "quoteFeeAmountCollected" - ] - } - }, - "required": ["signature", "status"] } } } @@ -4315,66 +9855,65 @@ } } }, - "/connectors/uniswap/router/quote-swap": { + "/connectors/pancakeswap-sol/clmm/quote-position": { "get": { - "tags": ["/connector/uniswap"], - "description": "Get an executable swap quote from Uniswap Universal Router", + "tags": ["/connector/pancakeswap-sol"], + "description": "Quote position amounts for PancakeSwap Solana CLMM (simplified)", "parameters": [ { - "schema": { - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, - "description": "The EVM network to use" + "description": "Solana network to use" }, { "schema": { "type": "string" }, - "example": "WETH", + "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", "in": "query", - "name": "baseToken", + "name": "poolAddress", "required": true, - "description": "First token in the trading pair" + "description": "PancakeSwap CLMM pool address" }, { - "schema": { "type": "string" }, - "example": "USDC", + "schema": { "type": "number" }, + "example": 150, "in": "query", - "name": "quoteToken", + "name": "lowerPrice", "required": true, - "description": "Second token in the trading pair" + "description": "Lower price bound for the position" }, { "schema": { "type": "number" }, - "example": 1, + "example": 250, "in": "query", - "name": "amount", + "name": "upperPrice", "required": true, - "description": "Amount of base token to trade" + "description": "Upper price bound for the position" }, { - "schema": { "enum": ["BUY", "SELL"], "type": "string" }, + "schema": { "type": "number" }, + "example": 0.01, "in": "query", - "name": "side", - "required": true, - "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" + "name": "baseTokenAmount", + "required": false, + "description": "Amount of base token to deposit" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "schema": { "type": "number" }, + "example": 2, "in": "query", - "name": "slippagePct", + "name": "quoteTokenAmount", "required": false, - "description": "Maximum acceptable slippage percentage" + "description": "Amount of quote token to deposit" }, { - "schema": { "default": "", "type": "string" }, + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, "in": "query", - "name": "walletAddress", + "name": "slippagePct", "required": false, - "description": "Wallet address for more accurate quotes (optional)" + "description": "Maximum acceptable slippage percentage" } ], "responses": { @@ -4385,30 +9924,19 @@ "schema": { "type": "object", "properties": { - "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, - "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, - "minAmountOut": { - "description": "Minimum amount of tokenOut that will be accepted", - "type": "number" - }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, - "routePath": { "description": "Human-readable route path", "type": "string" } + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, + "liquidity": {} }, "required": [ - "quoteId", - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "price", - "priceImpactPct", - "minAmountOut", - "maxAmountIn" + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" ] } } @@ -4417,89 +9945,96 @@ } } }, - "/connectors/uniswap/router/execute-quote": { - "post": { - "tags": ["/connector/uniswap"], - "description": "Execute a previously fetched quote from Uniswap Universal Router", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "walletAddress": { - "description": "Wallet address that will execute the swap", - "default": "", - "type": "string", - "example": "" - }, - "network": { - "description": "The blockchain network to use", - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], - "type": "string", - "example": "arbitrum" - }, - "quoteId": { - "description": "ID of the quote to execute", - "type": "string", - "example": "123e4567-e89b-12d3-a456-426614174000" - }, - "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 - } - }, - "required": ["quoteId"] - } - } + "/connectors/pancakeswap-sol/clmm/quote-swap": { + "get": { + "tags": ["/connector/pancakeswap-sol"], + "description": "Get swap quote for PancakeSwap Solana CLMM with fee and estimated price impact based on pool liquidity", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" }, - "required": true - }, - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, - "status": { - "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", - "type": "number" - }, - "data": { - "type": "object", - "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, - "baseTokenBalanceChange": { - "description": "Change in base token balance (negative for decrease)", - "type": "number" - }, - "quoteTokenBalanceChange": { - "description": "Change in quote token balance (negative for decrease)", - "type": "number" - } - }, - "required": [ - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "fee", - "baseTokenBalanceChange", - "quoteTokenBalanceChange" - ] - } + { + "schema": { "type": "string" }, + "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", + "in": "query", + "name": "poolAddress", + "required": false, + "description": "CLMM pool address (optional - can be looked up from tokens)" + }, + { + "schema": { "type": "string" }, + "example": "SOL", + "in": "query", + "name": "baseToken", + "required": true, + "description": "Base token symbol or address" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": true, + "description": "Quote token symbol or address" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount to swap" + }, + { + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, - "required": ["signature", "status"] + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] } } } @@ -4507,54 +10042,49 @@ } } }, - "/connectors/uniswap/router/execute-swap": { + "/connectors/pancakeswap-sol/clmm/execute-swap": { "post": { - "tags": ["/connector/uniswap"], - "description": "Quote and execute a token swap on Uniswap Universal Router in one step", + "tags": ["/connector/pancakeswap-sol"], + "description": "Execute a swap on PancakeSwap Solana CLMM", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "walletAddress": { - "description": "Wallet address that will execute the swap", - "default": "", - "type": "string", - "example": "" - }, "network": { - "description": "The blockchain network to use", - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], - "type": "string", - "example": "arbitrum" + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" }, - "baseToken": { - "description": "Token to determine swap direction", + "walletAddress": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "poolAddress": { + "description": "CLMM pool address (optional)", "type": "string", - "example": "WETH" + "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN" }, - "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 1 }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, "side": { - "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", + "description": "Trade direction", "enum": ["BUY", "SELL"], - "type": "string" + "default": "SELL", + "type": "string", + "example": "SELL" }, "slippagePct": { "minimum": 0, "maximum": 100, "description": "Maximum acceptable slippage percentage", - "default": 1, - "type": "number", - "example": 1 - }, - "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, - "maxGas": { - "description": "Maximum gas limit for the transaction", + "default": 2, "type": "number", - "example": 300000 + "example": 2 } }, "required": ["baseToken", "quoteToken", "amount", "side"] @@ -4571,27 +10101,18 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, - "status": { - "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, - "baseTokenBalanceChange": { - "description": "Change in base token balance (negative for decrease)", - "type": "number" - }, - "quoteTokenBalanceChange": { - "description": "Change in quote token balance (negative for decrease)", - "type": "number" - } + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } }, "required": [ "tokenIn", @@ -4612,116 +10133,67 @@ } } }, - "/connectors/uniswap/amm/pool-info": { - "get": { - "tags": ["/connector/uniswap"], - "description": "Get AMM pool information from Uniswap V2", - "parameters": [ - { "schema": { "type": "string" }, "in": "query", "name": "network", "required": false }, - { "schema": { "type": "string" }, "in": "query", "name": "poolAddress", "required": true } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "/connectors/pancakeswap-sol/clmm/open-position": { + "post": { + "tags": ["/connector/pancakeswap-sol"], + "description": "Open a new PancakeSwap Solana CLMM position with Token2022 NFT", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" }, - "required": [ - "address", - "baseTokenAddress", - "quoteTokenAddress", - "feePct", - "price", - "baseTokenAmount", - "quoteTokenAmount" - ] - } - } - } - } - } - } - }, - "/connectors/uniswap/amm/position-info": { - "get": { - "tags": ["/connector/uniswap"], - "description": "Get position information for a Uniswap V2 pool", - "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, - { - "schema": { "type": "string" }, - "example": "", - "in": "query", - "name": "walletAddress", - "required": false - }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "poolAddress": { "type": "string" }, - "walletAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "lpTokenAmount": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "price": { "type": "number" } + "walletAddress": { + "description": "Solana wallet address", + "default": "", + "type": "string" }, - "required": [ - "poolAddress", - "walletAddress", - "baseTokenAddress", - "quoteTokenAddress", - "lpTokenAmount", - "baseTokenAmount", - "quoteTokenAmount", - "price" - ] - } - } - } - } - } - } - }, - "/connectors/uniswap/amm/quote-swap": { - "get": { - "tags": ["/connector/uniswap"], - "description": "Get swap quote for Uniswap V2 AMM", - "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": false }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, - { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, - { - "schema": { "type": "string", "enum": ["BUY", "SELL"] }, - "example": "SELL", - "in": "query", - "name": "side", - "required": true + "poolAddress": { + "description": "PancakeSwap CLMM pool address", + "type": "string", + "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN" + }, + "lowerPrice": { + "description": "Lower price bound for the position", + "type": "number", + "example": 150 + }, + "upperPrice": { + "description": "Upper price bound for the position", + "type": "number", + "example": 250 + }, + "baseTokenAmount": { + "description": "Amount of base token to deposit", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { + "description": "Amount of quote token to deposit", + "type": "number", + "example": 2 + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + } + }, + "required": ["poolAddress", "lowerPrice", "upperPrice"] + } + } }, - { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } - ], + "required": true + }, "responses": { "200": { "description": "Default Response", @@ -4730,28 +10202,27 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": [ + "fee", + "positionAddress", + "positionRent", + "baseTokenAmountAdded", + "quoteTokenAmountAdded" + ] + } }, - "required": [ - "poolAddress", - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "price", - "minAmountOut", - "maxAmountIn", - "priceImpactPct" - ] + "required": ["signature", "status"] } } } @@ -4759,31 +10230,49 @@ } } }, - "/connectors/uniswap/amm/quote-liquidity": { - "get": { - "tags": ["/connector/uniswap"], - "description": "Get liquidity quote for a Uniswap V2 pool", - "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, - { - "schema": { "type": "number" }, - "example": 0.001, - "in": "query", - "name": "baseTokenAmount", - "required": true - }, - { - "schema": { "type": "number" }, - "example": 2.5, - "in": "query", - "name": "quoteTokenAmount", - "required": true + "/connectors/pancakeswap-sol/clmm/add-liquidity": { + "post": { + "tags": ["/connector/pancakeswap-sol"], + "description": "Add liquidity to an existing PancakeSwap Solana CLMM position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address", + "default": "", + "type": "string" + }, + "positionAddress": { "description": "Position NFT address", "type": "string", "example": "" }, + "baseTokenAmount": { + "description": "Amount of base token to add", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + } + }, + "required": ["positionAddress", "baseTokenAmount", "quoteTokenAmount"] + } + } }, - { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } - ], + "required": true + }, "responses": { "200": { "description": "Default Response", @@ -4792,19 +10281,20 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" } + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" }, + "newPositionAddress": { "type": "string" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } }, - "required": [ - "baseLimited", - "baseTokenAmount", - "quoteTokenAmount", - "baseTokenAmountMax", - "quoteTokenAmountMax" - ] + "required": ["signature", "status"] } } } @@ -4812,50 +10302,41 @@ } } }, - "/connectors/uniswap/amm/execute-swap": { + "/connectors/pancakeswap-sol/clmm/remove-liquidity": { "post": { - "tags": ["/connector/uniswap"], - "description": "Execute a swap on Uniswap V2 AMM using Router02", + "tags": ["/connector/pancakeswap-sol"], + "description": "Remove liquidity from a PancakeSwap Solana CLMM position", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "walletAddress": { - "description": "Wallet address that will execute the swap", - "default": "", - "type": "string" - }, "network": { - "description": "The EVM network to use", - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], "type": "string" }, - "poolAddress": { - "description": "Pool address (optional - can be looked up from tokens)", + "walletAddress": { + "description": "Solana wallet address", + "default": "", "type": "string" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "WETH" }, - "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount to swap", "type": "number", "example": 1 }, - "side": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, - "slippagePct": { + "positionAddress": { + "description": "Position NFT address to remove liquidity from", + "type": "string", + "example": "" + }, + "percentageToRemove": { "minimum": 0, "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 2, - "type": "number" - }, - "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, - "maxGas": { - "description": "Maximum gas limit for the transaction", + "description": "Percentage of liquidity to remove", "type": "number", - "example": 300000 + "example": 100 } }, - "required": ["baseToken", "amount", "side"] + "required": ["positionAddress", "percentageToRemove"] } } }, @@ -4874,23 +10355,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, - "required": [ - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "fee", - "baseTokenBalanceChange", - "quoteTokenBalanceChange" - ] + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } }, "required": ["signature", "status"] @@ -4901,10 +10370,10 @@ } } }, - "/connectors/uniswap/amm/add-liquidity": { + "/connectors/pancakeswap-sol/clmm/collect-fees": { "post": { - "tags": ["/connector/uniswap"], - "description": "Add liquidity to a Uniswap V2 pool", + "tags": ["/connector/pancakeswap-sol"], + "description": "Collect accumulated fees from a PancakeSwap Solana CLMM position (removes 1% liquidity)", "requestBody": { "content": { "application/json": { @@ -4912,34 +10381,19 @@ "type": "object", "properties": { "network": { - "description": "The EVM network to use", - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], "type": "string" }, "walletAddress": { - "description": "Wallet address that will add liquidity", - "default": "", + "description": "Solana wallet address", + "default": "", "type": "string" }, - "poolAddress": { "description": "Address of the Uniswap V2 pool", "type": "string" }, - "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, - "slippagePct": { - "minimum": 0, - "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 2, - "type": "number" - }, - "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 - } + "positionAddress": { "description": "Position NFT address", "type": "string", "example": "" } }, - "required": ["poolAddress", "baseTokenAmount", "quoteTokenAmount"] + "required": ["positionAddress"] } } }, @@ -4959,10 +10413,10 @@ "type": "object", "properties": { "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, - "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } }, "required": ["signature", "status"] @@ -4973,10 +10427,10 @@ } } }, - "/connectors/uniswap/amm/remove-liquidity": { + "/connectors/pancakeswap-sol/clmm/close-position": { "post": { - "tags": ["/connector/uniswap"], - "description": "Remove liquidity from a Uniswap V2 pool", + "tags": ["/connector/pancakeswap-sol"], + "description": "Close a PancakeSwap Solana CLMM position and remove all liquidity and fees if present", "requestBody": { "content": { "application/json": { @@ -4984,31 +10438,19 @@ "type": "object", "properties": { "network": { - "description": "The EVM network to use", - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], "type": "string" }, "walletAddress": { - "description": "Wallet address that will remove liquidity", - "default": "", + "description": "Solana wallet address", + "default": "", "type": "string" }, - "poolAddress": { "description": "Address of the Uniswap V2 pool", "type": "string" }, - "percentageToRemove": { - "minimum": 0, - "maximum": 100, - "description": "Percentage of liquidity to remove", - "type": "number" - }, - "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 - } + "positionAddress": { "description": "Position NFT address to close", "type": "string", "example": "" } }, - "required": ["poolAddress", "percentageToRemove"] + "required": ["positionAddress"] } } }, @@ -5028,10 +10470,20 @@ "type": "object", "properties": { "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, - "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + "required": [ + "fee", + "positionRentRefunded", + "baseTokenAmountRemoved", + "quoteTokenAmountRemoved", + "baseFeeAmountCollected", + "quoteFeeAmountCollected" + ] } }, "required": ["signature", "status"] @@ -5042,15 +10494,24 @@ } } }, - "/connectors/uniswap/clmm/pool-info": { + "/connectors/osmosis/amm/pool-info": { "get": { - "tags": ["/connector/uniswap"], - "description": "Get CLMM pool information from Uniswap V3", + "tags": ["/connector/osmosis"], + "description": "Get AMM pool information from Osmosis", "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } + { + "schema": { "type": "string", "default": "mainnet", "enum": ["testnet", "mainnet"] }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { "type": "string" }, + "example": "osmo1500hy75krs9e8t50aav6fahk8sxhajn9ctp40qwvvn8tcprkk6wszun4a5", + "in": "query", + "name": "poolAddress", + "required": true + } ], "responses": { "200": { @@ -5063,23 +10524,19 @@ "address": { "type": "string" }, "baseTokenAddress": { "type": "string" }, "quoteTokenAddress": { "type": "string" }, - "binStep": { "type": "number" }, "feePct": { "type": "number" }, "price": { "type": "number" }, "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "activeBinId": { "type": "number" } + "quoteTokenAmount": { "type": "number" } }, "required": [ "address", "baseTokenAddress", "quoteTokenAddress", - "binStep", "feePct", "price", "baseTokenAmount", - "quoteTokenAmount", - "activeBinId" + "quoteTokenAmount" ] } } @@ -5088,27 +10545,20 @@ } } }, - "/connectors/uniswap/clmm/position-info": { + "/connectors/osmosis/amm/position-info": { "get": { - "tags": ["/connector/uniswap"], - "description": "Get position information for a Uniswap V3 position", + "tags": ["/connector/osmosis"], + "description": "Get position information for a osmosis AMM pool", "parameters": [ { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { "schema": { "type": "string" }, - "example": "1234", - "in": "query", - "name": "positionAddress", - "required": true, - "description": "Position NFT token ID" - }, - { - "schema": { "type": "string" }, - "example": "", + "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", "in": "query", "name": "walletAddress", "required": false - } + }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true } ], "responses": { "200": { @@ -5118,33 +10568,23 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, "poolAddress": { "type": "string" }, + "walletAddress": { "type": "string" }, "baseTokenAddress": { "type": "string" }, "quoteTokenAddress": { "type": "string" }, + "lpTokenAmount": { "type": "number" }, "baseTokenAmount": { "type": "number" }, "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, "price": { "type": "number" } }, "required": [ - "address", "poolAddress", + "walletAddress", "baseTokenAddress", "quoteTokenAddress", + "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount", - "baseFeeAmount", - "quoteFeeAmount", - "lowerBinId", - "upperBinId", - "lowerPrice", - "upperPrice", "price" ] } @@ -5154,104 +10594,35 @@ } } }, - "/connectors/uniswap/clmm/positions-owned": { + "/connectors/osmosis/amm/quote-swap": { "get": { - "tags": ["/connector/uniswap"], - "description": "Get all Uniswap V3 positions owned by a wallet", + "tags": ["/connector/osmosis"], + "description": "Get a swap quote using Osmosis router", "parameters": [ { - "schema": { "default": "base", "type": "string" }, - "example": "base", + "schema": { "type": "string", "default": "mainnet", "enum": ["testnet", "mainnet"] }, "in": "query", "name": "network", "required": false }, + { "schema": { "type": "string" }, "example": "ION", "in": "query", "name": "baseToken", "required": true }, + { "schema": { "type": "string" }, "example": "OSMO", "in": "query", "name": "quoteToken", "required": true }, + { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, { - "schema": { "type": "string" }, - "example": "", + "schema": { "type": "string", "enum": ["BUY", "SELL"] }, + "example": "SELL", "in": "query", - "name": "walletAddress", + "name": "side", "required": true - } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } - }, - "required": [ - "address", - "poolAddress", - "baseTokenAddress", - "quoteTokenAddress", - "baseTokenAmount", - "quoteTokenAmount", - "baseFeeAmount", - "quoteFeeAmount", - "lowerBinId", - "upperBinId", - "lowerPrice", - "upperPrice", - "price" - ] - } - } - } - } - } - } - } - }, - "/connectors/uniswap/clmm/quote-position": { - "get": { - "tags": ["/connector/uniswap"], - "description": "Get a quote for opening a position on Uniswap V3", - "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, - { "schema": { "type": "number" }, "example": 1000, "in": "query", "name": "lowerPrice", "required": true }, - { "schema": { "type": "number" }, "example": 4000, "in": "query", "name": "upperPrice", "required": true }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, - { - "schema": { "type": "number" }, - "example": 0.001, - "in": "query", - "name": "baseTokenAmount", - "required": false - }, - { - "schema": { "type": "number" }, - "example": 3, - "in": "query", - "name": "quoteTokenAmount", - "required": false }, + { "schema": { "type": "number" }, "example": 0.5, "in": "query", "name": "slippagePct", "required": false }, { - "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "schema": { "type": "string" }, + "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", "in": "query", - "name": "slippagePct", + "name": "walletAddress", "required": false - }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } + } ], "responses": { "200": { @@ -5261,19 +10632,27 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" }, - "liquidity": {} + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, "required": [ - "baseLimited", - "baseTokenAmount", - "quoteTokenAmount", - "baseTokenAmountMax", - "quoteTokenAmountMax" + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" ] } } @@ -5282,30 +10661,19 @@ } } }, - "/connectors/uniswap/clmm/quote-swap": { + "/connectors/osmosis/amm/positions-owned": { "get": { - "tags": ["/connector/uniswap"], - "description": "Get swap quote for Uniswap V3 CLMM", + "tags": ["/connector/osmosis"], + "description": "Get all AMM positions for wallet address", "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { "schema": { "type": "string", "default": "mainnet" }, "in": "query", "name": "network", "required": false }, { "schema": { "type": "string" }, + "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", "in": "query", - "name": "poolAddress", - "required": false, - "description": "Pool address (optional - can be looked up from baseToken and quoteToken)" - }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, - { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, - { - "schema": { "type": "string", "enum": ["BUY", "SELL"] }, - "example": "SELL", - "in": "query", - "name": "side", - "required": true - }, - { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } + "name": "walletAddress", + "required": false + } ], "responses": { "200": { @@ -5315,27 +10683,36 @@ "schema": { "type": "object", "properties": { + "address": { "type": "string" }, "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } }, "required": [ + "address", "poolAddress", - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "price", - "minAmountOut", - "maxAmountIn", - "priceImpactPct" + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" ] } } @@ -5344,26 +10721,25 @@ } } }, - "/connectors/uniswap/clmm/execute-swap": { + "/connectors/osmosis/amm/execute-swap": { "post": { - "tags": ["/connector/uniswap"], - "description": "Execute a swap on Uniswap V3 CLMM using SwapRouter02", + "tags": ["/connector/osmosis"], + "description": "Execute a swap using Osmosis Order Router", "requestBody": { "content": { "application/json": { "schema": { "type": "object", - "properties": { - "walletAddress": { "type": "string", "example": "" }, - "network": { "type": "string", "default": "base" }, - "poolAddress": { "type": "string", "example": "" }, + "properties": { + "network": { "type": "string", "default": "mainnet", "enum": ["testnet", "mainnet"] }, + "walletAddress": { "type": "string", "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" }, "baseToken": { "type": "string", "example": "WETH" }, "quoteToken": { "type": "string", "example": "USDC" }, "amount": { "type": "number", "example": 0.001 }, "side": { "type": "string", "enum": ["BUY", "SELL"], "example": "SELL" }, - "slippagePct": { "type": "number", "example": 1 } + "slippagePct": { "type": "number", "example": 0.5 } }, - "required": ["baseToken", "amount", "side"] + "required": ["walletAddress", "baseToken", "quoteToken", "amount", "side"] } } }, @@ -5409,10 +10785,10 @@ } } }, - "/connectors/uniswap/clmm/open-position": { + "/connectors/osmosis/amm/add-liquidity": { "post": { - "tags": ["/connector/uniswap"], - "description": "Open a new liquidity position in a Uniswap V3 pool", + "tags": ["/connector/osmosis"], + "description": "Add liquidity to a Osmosis GAMM pool", "requestBody": { "content": { "application/json": { @@ -5420,17 +10796,15 @@ "type": "object", "properties": { "network": { "type": "string", "default": "base" }, - "walletAddress": { "type": "string", "example": "" }, - "lowerPrice": { "type": "number", "example": 1000 }, - "upperPrice": { "type": "number", "example": 4000 }, + "walletAddress": { "type": "string", "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" }, "poolAddress": { "type": "string", "example": "" }, "baseTokenAmount": { "type": "number", "example": 0.001 }, - "quoteTokenAmount": { "type": "number", "example": 3 }, + "quoteTokenAmount": { "type": "number", "example": 2.5 }, "slippagePct": { "type": "number", "example": 1 }, - "baseToken": { "type": "string", "example": "WETH" }, - "quoteToken": { "type": "string", "example": "USDC" } + "baseToken": { "type": "string", "example": "OSMO" }, + "quoteToken": { "type": "string", "example": "ION" } }, - "required": ["lowerPrice", "upperPrice", "poolAddress"] + "required": ["poolAddress", "baseTokenAmount", "quoteTokenAmount"] } } }, @@ -5450,18 +10824,10 @@ "type": "object", "properties": { "fee": { "type": "number" }, - "positionAddress": { "type": "string" }, - "positionRent": { "type": "number" }, "baseTokenAmountAdded": { "type": "number" }, "quoteTokenAmountAdded": { "type": "number" } }, - "required": [ - "fee", - "positionAddress", - "positionRent", - "baseTokenAmountAdded", - "quoteTokenAmountAdded" - ] + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } }, "required": ["signature", "status"] @@ -5472,50 +10838,272 @@ } } }, - "/connectors/uniswap/clmm/add-liquidity": { + "/connectors/osmosis/amm/remove-liquidity": { "post": { - "tags": ["/connector/uniswap"], - "description": "Add liquidity to an existing Uniswap V3 position", + "tags": ["/connector/osmosis"], + "description": "Remove liquidity from an Osmosis GAMM pool", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { - "description": "The EVM network to use", - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], - "type": "string" + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" }, + "poolAddress": { "type": "string", "example": "" }, + "percentageToRemove": { "type": "number", "example": 100 } + }, + "required": ["poolAddress", "percentageToRemove"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + } }, - "walletAddress": { - "description": "Wallet address that will add liquidity", - "default": "", - "type": "string" + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/osmosis/clmm/pool-info": { + "get": { + "tags": ["/connector/osmosis"], + "description": "Get CLMM pool information from Osmosis", + "parameters": [ + { + "schema": { "type": "string", "default": "mainnet", "enum": ["testnet", "mainnet"] }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { "type": "string" }, + "example": "osmo146zct0tppdd4yyrdpn8u8j82yvhwvpx23pmy7yh45xj0ttya305s2edl6v", + "in": "query", + "name": "poolAddress", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } }, - "positionAddress": { "description": "NFT token ID of the position", "type": "string" }, - "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, - "slippagePct": { - "minimum": 0, - "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 2, - "type": "number" + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount" + ] + } + } + } + } + } + } + }, + "/connectors/osmosis/clmm/position-info": { + "get": { + "tags": ["/connector/osmosis"], + "description": "Get position information for a osmosis CLMM pool", + "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { + "schema": { "type": "string" }, + "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "in": "query", + "name": "walletAddress", + "required": false + }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": false } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } }, - "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 - } - }, - "required": ["positionAddress", "baseTokenAmount", "quoteTokenAmount"] + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/osmosis/clmm/positions-owned": { + "get": { + "tags": ["/connector/osmosis"], + "description": "Get all CLMM positions for wallet address. Warning: Spams RPC to do so (only way for CL pools).", + "parameters": [ + { "schema": { "type": "string", "default": "mainnet" }, "in": "query", "name": "network", "required": false }, + { + "schema": { "type": "string" }, + "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "in": "query", + "name": "walletAddress", + "required": false + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" }, + "rewardTokenAddress": { "type": "string" }, + "rewardAmount": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } } } + } + } + } + }, + "/connectors/osmosis/clmm/quote-position": { + "get": { + "tags": ["/connector/osmosis"], + "description": "Get a quote for opening a position on Osmosis CL", + "parameters": [ + { + "schema": { "type": "string", "default": "base" }, + "example": "base", + "in": "query", + "name": "network", + "required": false + }, + { "schema": { "type": "number" }, "example": 2000, "in": "query", "name": "lowerPrice", "required": true }, + { "schema": { "type": "number" }, "example": 4000, "in": "query", "name": "upperPrice", "required": true }, + { + "schema": { + "type": "string", + "default": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6" + }, + "example": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6", + "in": "query", + "name": "poolAddress", + "required": true + }, + { + "schema": { "type": "number" }, + "example": 0.001, + "in": "query", + "name": "baseTokenAmount", + "required": false }, - "required": true - }, + { + "schema": { "type": "number" }, + "example": 3, + "in": "query", + "name": "quoteTokenAmount", + "required": false + }, + { + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "in": "query", + "name": "slippagePct", + "required": false + } + ], "responses": { "200": { "description": "Default Response", @@ -5524,19 +11112,20 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "data": { - "type": "object", - "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } - }, - "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] - } + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, + "liquidity": {} }, - "required": ["signature", "status"] + "required": [ + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" + ] } } } @@ -5544,27 +11133,36 @@ } } }, - "/connectors/uniswap/clmm/remove-liquidity": { - "post": { - "tags": ["/connector/uniswap"], - "description": "Remove liquidity from a Uniswap V3 position", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "network": { "type": "string", "default": "base" }, - "walletAddress": { "type": "string", "example": "" }, - "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" }, - "percentageToRemove": { "type": "number", "minimum": 0, "maximum": 100, "example": 50 } - }, - "required": ["positionAddress", "percentageToRemove"] - } - } + "/connectors/osmosis/clmm/quote-swap": { + "get": { + "tags": ["/connector/osmosis"], + "description": "Get a swap quote using Osmosis router", + "parameters": [ + { + "schema": { "type": "string", "default": "mainnet", "enum": ["testnet", "mainnet"] }, + "in": "query", + "name": "network", + "required": false }, - "required": true - }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": true }, + { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, + { + "schema": { "type": "string", "enum": ["BUY", "SELL"] }, + "example": "SELL", + "in": "query", + "name": "side", + "required": true + }, + { "schema": { "type": "number" }, "example": 0.5, "in": "query", "name": "slippagePct", "required": false }, + { + "schema": { "type": "string" }, + "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "in": "query", + "name": "walletAddress", + "required": false + } + ], "responses": { "200": { "description": "Default Response", @@ -5573,19 +11171,28 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "data": { - "type": "object", - "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } - }, - "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] - } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, - "required": ["signature", "status"] + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] } } } @@ -5593,21 +11200,25 @@ } } }, - "/connectors/uniswap/clmm/collect-fees": { + "/connectors/osmosis/clmm/execute-swap": { "post": { - "tags": ["/connector/uniswap"], - "description": "Collect fees from a Uniswap V3 position", + "tags": ["/connector/osmosis"], + "description": "Execute a swap using Osmosis Order Router", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { "type": "string", "default": "base" }, - "walletAddress": { "type": "string", "example": "" }, - "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" } + "network": { "type": "string", "default": "mainnet", "enum": ["testnet", "mainnet"] }, + "walletAddress": { "type": "string", "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" }, + "baseToken": { "type": "string", "example": "WETH" }, + "quoteToken": { "type": "string", "example": "USDC" }, + "amount": { "type": "number", "example": 0.001 }, + "side": { "type": "string", "enum": ["BUY", "SELL"], "example": "SELL" }, + "slippagePct": { "type": "number", "example": 0.5 } }, - "required": ["positionAddress"] + "required": ["walletAddress", "baseToken", "quoteToken", "amount", "side"] } } }, @@ -5626,11 +11237,23 @@ "data": { "type": "object", "properties": { + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, "fee": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } }, - "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] } }, "required": ["signature", "status"] @@ -5641,21 +11264,28 @@ } } }, - "/connectors/uniswap/clmm/close-position": { + "/connectors/osmosis/clmm/open-position": { "post": { - "tags": ["/connector/uniswap"], - "description": "Close a Uniswap V3 position by removing all liquidity and collecting fees", + "tags": ["/connector/osmosis"], + "description": "Open a new liquidity position in an Osmosis CL Pool", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { "type": "string" }, - "walletAddress": { "type": "string" }, - "positionAddress": { "type": "string" } + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" }, + "lowerPrice": { "type": "number", "example": 1000 }, + "upperPrice": { "type": "number", "example": 4000 }, + "poolAddress": { "type": "string", "example": "" }, + "baseTokenAmount": { "type": "number", "example": 0.001 }, + "quoteTokenAmount": { "type": "number", "example": 3 }, + "slippagePct": { "type": "number", "example": 1 }, + "baseToken": { "type": "string", "example": "ION" }, + "quoteToken": { "type": "string", "example": "OSMO" } }, - "required": ["positionAddress"] + "required": ["lowerPrice", "upperPrice", "poolAddress"] } } }, @@ -5675,100 +11305,51 @@ "type": "object", "properties": { "fee": { "type": "number" }, - "positionRentRefunded": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": [ "fee", - "positionRentRefunded", - "baseTokenAmountRemoved", - "quoteTokenAmountRemoved", - "baseFeeAmountCollected", - "quoteFeeAmountCollected" + "positionAddress", + "positionRent", + "baseTokenAmountAdded", + "quoteTokenAmountAdded" ] } }, "required": ["signature", "status"] - } - } - } - } - } - } - }, - "/connectors/0x/router/quote-swap": { - "get": { - "tags": ["/connector/0x"], - "description": "Get a swap quote from 0x. Use indicativePrice=true for price discovery only, or false/undefined for executable quotes", - "parameters": [ - { - "schema": { - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "mainnet", "optimism", "polygon"], - "type": "string" - }, - "in": "query", - "name": "network", - "required": false, - "description": "The EVM network to use" - }, - { - "schema": { "type": "string" }, - "example": "WETH", - "in": "query", - "name": "baseToken", - "required": true, - "description": "First token in the trading pair" - }, - { - "schema": { "type": "string" }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": true, - "description": "Second token in the trading pair" - }, - { - "schema": { "type": "number" }, - "example": 1, - "in": "query", - "name": "amount", - "required": true, - "description": "Amount of base token to trade" - }, - { - "schema": { "enum": ["BUY", "SELL"], "type": "string" }, - "in": "query", - "name": "side", - "required": true, - "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" - }, - { - "schema": { "minimum": 0, "maximum": 100, "type": "number" }, - "example": 1, - "in": "query", - "name": "slippagePct", - "required": false, - "description": "Maximum acceptable slippage percentage" - }, - { - "schema": { "default": true, "type": "boolean" }, - "in": "query", - "name": "indicativePrice", - "required": false, - "description": "If true, returns indicative pricing only (no commitment). If false, returns firm quote ready for execution" - }, - { - "schema": { "type": "string" }, - "in": "query", - "name": "takerAddress", - "required": false, - "description": "Ethereum wallet address that will execute the swap (optional for quotes)" + } + } + } } - ], + } + } + }, + "/connectors/osmosis/clmm/add-liquidity": { + "post": { + "tags": ["/connector/osmosis"], + "description": "Add liquidity to an existing Osmosis CL position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "0x..." }, + "positionAddress": { "type": "string", "description": "Position NFT token ID" }, + "baseTokenAmount": { "type": "number", "example": 0.1 }, + "quoteTokenAmount": { "type": "number", "example": 200 }, + "slippagePct": { "type": "number", "example": 1 } + }, + "required": ["positionAddress", "baseTokenAmount", "quoteTokenAmount"] + } + } + }, + "required": true + }, "responses": { "200": { "description": "Default Response", @@ -5777,44 +11358,20 @@ "schema": { "type": "object", "properties": { - "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, - "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, - "minAmountOut": { - "description": "Minimum amount of tokenOut that will be accepted", - "type": "number" - }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, - "expirationTime": { - "description": "Unix timestamp when this quote expires (only for firm quotes)", - "type": "number" - }, - "gasEstimate": { "description": "Estimated gas required for the swap", "type": "string" }, - "sources": { "description": "Liquidity sources used for this quote", "type": "array", "items": {} }, - "allowanceTarget": { - "description": "Contract address that needs token approval", - "type": "string" - }, - "to": { "description": "Contract address to send transaction to", "type": "string" }, - "data": { "description": "Encoded transaction data", "type": "string" }, - "value": { "description": "ETH value to send with transaction", "type": "string" } + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" }, + "newPositionAddress": { "type": "string" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } }, - "required": [ - "quoteId", - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "price", - "priceImpactPct", - "minAmountOut", - "maxAmountIn", - "gasEstimate" - ] + "required": ["signature", "status"] } } } @@ -5822,41 +11379,22 @@ } } }, - "/connectors/0x/router/execute-quote": { + "/connectors/osmosis/clmm/remove-liquidity": { "post": { - "tags": ["/connector/0x"], - "description": "Execute a previously fetched quote from 0x", + "tags": ["/connector/osmosis"], + "description": "Remove liquidity from an Osmosis CL Position", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "walletAddress": { - "description": "Wallet address that will execute the swap", - "default": "", - "type": "string" - }, - "network": { - "description": "The blockchain network to use", - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "mainnet", "optimism", "polygon"], - "type": "string", - "example": "arbitrum" - }, - "quoteId": { - "description": "ID of the quote to execute", - "type": "string", - "example": "123e4567-e89b-12d3-a456-426614174000" - }, - "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 1000000 - } + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" }, + "positionAddress": { "type": "string", "description": "Position address", "example": "1234" }, + "percentageToRemove": { "type": "number", "minimum": 0, "maximum": 100, "example": 50 } }, - "required": ["quoteId"] + "required": ["positionAddress", "percentageToRemove"] } } }, @@ -5870,37 +11408,16 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, - "status": { - "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, - "baseTokenBalanceChange": { - "description": "Change in base token balance (negative for decrease)", - "type": "number" - }, - "quoteTokenBalanceChange": { - "description": "Change in quote token balance (negative for decrease)", - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, - "required": [ - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "fee", - "baseTokenBalanceChange", - "quoteTokenBalanceChange" - ] + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } }, "required": ["signature", "status"] @@ -5911,56 +11428,69 @@ } } }, - "/connectors/0x/router/execute-swap": { + "/connectors/osmosis/clmm/collect-fees": { "post": { - "tags": ["/connector/0x"], - "description": "Quote and execute a token swap on 0x in one step", + "tags": ["/connector/osmosis"], + "description": "Collect fees from an Osmosis CL position", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "walletAddress": { - "description": "Wallet address that will execute the swap", - "default": "", - "type": "string", - "example": "" - }, - "network": { - "description": "The blockchain network to use", - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "mainnet", "optimism", "polygon"], - "type": "string", - "example": "arbitrum" - }, - "baseToken": { - "description": "Token to determine swap direction", - "type": "string", - "example": "WETH" - }, - "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 1 }, - "side": { - "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", - "enum": ["BUY", "SELL"], - "type": "string" - }, - "slippagePct": { - "minimum": 0, - "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "type": "number", - "example": 1 + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" }, + "positionAddress": { "type": "string", "description": "Position address", "example": "1234" } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] + } }, - "gasPrice": { "description": "Gas price in wei for the transaction", "type": "string" }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 - } + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/osmosis/clmm/close-position": { + "post": { + "tags": ["/connector/osmosis"], + "description": "Close an Osmosis CL position by removing all liquidity and collecting fees", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "0x..." }, + "positionAddress": { "type": "string", "description": "Position NFT token ID" } }, - "required": ["baseToken", "quoteToken", "amount", "side"] + "required": ["positionAddress"] } } }, @@ -5974,36 +11504,25 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, - "status": { - "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, - "baseTokenBalanceChange": { - "description": "Change in base token balance (negative for decrease)", - "type": "number" - }, - "quoteTokenBalanceChange": { - "description": "Change in quote token balance (negative for decrease)", - "type": "number" - } + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": [ - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", "fee", - "baseTokenBalanceChange", - "quoteTokenBalanceChange" + "positionRentRefunded", + "baseTokenAmountRemoved", + "quoteTokenAmountRemoved", + "baseFeeAmountCollected", + "quoteFeeAmountCollected" ] } }, @@ -6022,12 +11541,18 @@ { "name": "/wallet", "description": "Wallet management endpoints" }, { "name": "/tokens", "description": "Token management endpoints" }, { "name": "/pools", "description": "Pool management endpoints" }, + { "name": "/trading/swap", "description": "Unified cross-chain swap endpoints" }, + { "name": "/trading/clmm", "description": "Unified cross-chain CLMM (Concentrated Liquidity) endpoints" }, { "name": "/chain/solana", "description": "Solana and SVM-based chain endpoints" }, { "name": "/chain/ethereum", "description": "Ethereum and EVM-based chain endpoints" }, + { "name": "/chain/cosmos", "description": "Cosmos (via Osmosis RPC) chain endpoints" }, { "name": "/connector/jupiter", "description": "Jupiter connector endpoints" }, { "name": "/connector/meteora", "description": "Meteora connector endpoints" }, { "name": "/connector/raydium", "description": "Raydium connector endpoints" }, { "name": "/connector/uniswap", "description": "Uniswap connector endpoints" }, - { "name": "/connector/0x", "description": "0x connector endpoints" } + { "name": "/connector/0x", "description": "0x connector endpoints" }, + { "name": "/connector/pancakeswap-sol", "description": "PancakeSwap Solana connector endpoints" }, + { "name": "/connector/pancakeswap", "description": "PancakeSwap EVM connector endpoints" }, + { "name": "/connector/osmosis", "description": "Osmosis connector endpoints" } ] } diff --git a/package.json b/package.json index 8befe150b1..e02e93160e 100644 --- a/package.json +++ b/package.json @@ -25,19 +25,26 @@ "setup:with-defaults": "bash ./gateway-setup.sh --with-defaults", "start": "START_SERVER=true node dist/index.js", "copy-files": "copyfiles 'src/templates/namespace/*.json' 'src/templates/*.yml' 'src/templates/chains/**/*.yml' 'src/templates/connectors/*.yml' 'src/templates/tokens/**/*.json' 'src/templates/pools/*.json' 'src/templates/rpc/*.yml' dist && copyfiles -u 1 'src/connectors/pancakeswap-sol/idl/*.json' dist", - "test": "GATEWAY_TEST_MODE=dev jest --verbose", + "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" GATEWAY_TEST_MODE=dev jest --verbose", "test:clear-cache": "jest --clearCache", - "test:debug": "GATEWAY_TEST_MODE=dev jest --watch --runInBand", - "test:unit": "GATEWAY_TEST_MODE=dev jest --runInBand ./test/", - "test:cov": "GATEWAY_TEST_MODE=dev jest --runInBand --coverage ./test/", - "test:scripts": "GATEWAY_TEST_MODE=dev jest --runInBand ./test-scripts/*.test.ts", + "test:debug": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" GATEWAY_TEST_MODE=dev jest --watch --runInBand", + "test:unit": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" GATEWAY_TEST_MODE=dev jest --runInBand ./test/", + "test:cov": "NODE_OPTIONS=\"$NODE_OPTIONS --max-old-space-size=32000 --experimental-vm-modules\" GATEWAY_TEST_MODE=dev jest --runInBand --coverage ./test/", + "test:scripts": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" GATEWAY_TEST_MODE=dev jest --runInBand ./test-scripts/*.test.ts", "typecheck": "tsc --noEmit", "generate:openapi": "curl http://localhost:15888/docs/json -o openapi.json && echo 'OpenAPI spec saved to openapi.json'", "rebuild-bigint": "cd node_modules/bigint-buffer && pnpm run rebuild", "prepare": "pnpm husky" }, "dependencies": { + "@chain-registry/types": "^2.0.104", "@coral-xyz/anchor": "^0.30.1", + "@cosmjs/amino": "^0.32.4", + "@cosmjs/cosmwasm-stargate": "^0.32.3", + "@cosmjs/encoding": "^0.32.4", + "@cosmjs/proto-signing": "0.32.3", + "@cosmjs/stargate": "^0.32.4", + "@cosmjs/tendermint-rpc": "^0.32.4", "@ethersproject/abstract-provider": "5.7.0", "@ethersproject/address": "5.7.0", "@ethersproject/contracts": "5.7.0", @@ -55,6 +62,8 @@ "@ledgerhq/hw-transport-node-hid-singleton": "^6.31.8", "@meteora-ag/dlmm": "1.7.5", "@orca-so/common-sdk": "^0.6.11", + "@osmonauts/math": "^1.18.0", + "@osmonauts/utils": "^1.18.0", "@pancakeswap/permit2-sdk": "^1.1.5", "@pancakeswap/sdk": "^5.8.16", "@pancakeswap/smart-router": "^7.5.2", @@ -86,6 +95,7 @@ "app-root-path": "^3.1.0", "axios": "^1.8.4", "bigint-buffer": "1.1.5", + "bignumber.js": "^9.3.1", "bn.js": "5.2.1", "brotli": "1.3.2", "dayjs": "^1.11.13", @@ -97,9 +107,12 @@ "fastify": "^4.29.0", "fastify-type-provider-zod": "^2.1.0", "fs-extra": "^10.1.0", + "http-errors": "^2.0.0", "level": "^8.0.1", "mathjs": "^10.6.4", "minimist": "^1.2.8", + "osmo-query": "^16.14.0", + "osmojs": "^16.15.0", "pino-pretty": "^11.3.0", "pnpm": "^10.10.0", "snake-case": "^4.0.0", @@ -113,7 +126,7 @@ }, "devDependencies": { "@types/app-root-path": "^1.2.8", - "@types/bn.js": "5.1.6", + "@types/bn.js": "latest", "@types/bs58": "^4.0.4", "@types/express": "^4.17.21", "@types/fs-extra": "^9.0.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bad4336b6..27ce7a9f18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,9 +19,30 @@ importers: .: dependencies: + '@chain-registry/types': + specifier: ^2.0.104 + version: 2.0.104 '@coral-xyz/anchor': specifier: ^0.30.1 version: 0.30.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@cosmjs/amino': + specifier: ^0.37.0 + version: 0.37.0 + '@cosmjs/cosmwasm-stargate': + specifier: ^0.37.0 + version: 0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/encoding': + specifier: ^0.37.0 + version: 0.37.0 + '@cosmjs/proto-signing': + specifier: 0.32.3 + version: 0.32.3 + '@cosmjs/stargate': + specifier: ^0.37.0 + version: 0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/tendermint-rpc': + specifier: ^0.37.0 + version: 0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@ethersproject/abstract-provider': specifier: 5.7.0 version: 5.7.0 @@ -73,6 +94,12 @@ importers: '@orca-so/common-sdk': specifier: ^0.6.11 version: 0.6.11(@solana/spl-token@0.4.8(@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)) + '@osmonauts/math': + specifier: ^1.18.0 + version: 1.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@osmonauts/utils': + specifier: ^1.18.0 + version: 1.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@pancakeswap/permit2-sdk': specifier: ^1.1.5 version: 1.1.5(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4) @@ -166,6 +193,9 @@ importers: bigint-buffer: specifier: 1.1.5 version: 1.1.5 + bignumber.js: + specifier: ^9.3.1 + version: 9.3.1 bn.js: specifier: 5.2.1 version: 5.2.1 @@ -199,6 +229,9 @@ importers: fs-extra: specifier: ^10.1.0 version: 10.1.0 + http-errors: + specifier: ^2.0.0 + version: 2.0.0 level: specifier: ^8.0.1 version: 8.0.1 @@ -208,6 +241,12 @@ importers: minimist: specifier: ^0.2.4 version: 0.2.4 + osmo-query: + specifier: ^16.14.0 + version: 16.14.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + osmojs: + specifier: ^16.15.0 + version: 16.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) pino-pretty: specifier: ^11.3.0 version: 11.3.0 @@ -696,10 +735,23 @@ packages: deprecated: Bundlr is now Irys - please switch to @irys/sdk - this package will remain compatible with Irys for the foreseeable future. hasBin: true + '@chain-registry/types@0.50.255': + resolution: {integrity: sha512-wpr6Sov7yfwmV6RYA96uhQQPLK5JUxuSPYSLVks8uxXLnvtFh7GRw8SkFc1LzBRuwkTUyVkwYlYjPmzR9DrMcg==} + + '@chain-registry/types@2.0.104': + resolution: {integrity: sha512-i/w2dx7pA2or4cnZmlXgFS8mCfmMeo3KPJpuJrfexdtbbZ5xOGeOX1k2TL40Dm3HuIce5hmGJrpFnng/ndQdgg==} + + '@chain-registry/utils@1.51.255': + resolution: {integrity: sha512-e85d5k7q5snT/55WrsEV3DxXKeZDE6bzQD8vNSE+8ct0rKiFpsdxj8XKcl1ixgfGxbBavZ2EptpjWhy56C4LFw==} + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} + '@confio/ics23@0.6.8': + resolution: {integrity: sha512-wB6uo+3A50m0sW/EWcU64xpV/8wShZ6bMTa7pF8eYsTrSkQA7oLUIJcs/wb8g4y2Oyq701BaGiO6n/ak5WXO1w==} + deprecated: Unmaintained. The codebase for this package was moved to https://github.com/cosmos/ics23 but then the JS implementation was removed in https://github.com/cosmos/ics23/pull/353. Please consult the maintainers of https://github.com/cosmos for further assistance. + '@coral-xyz/anchor-errors@0.30.1': resolution: {integrity: sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ==} engines: {node: '>=10'} @@ -728,6 +780,85 @@ packages: peerDependencies: '@solana/web3.js': ^1.69.0 + '@cosmjs/amino@0.32.3': + resolution: {integrity: sha512-G4zXl+dJbqrz1sSJ56H/25l5NJEk/pAPIr8piAHgbXYw88OdAOlpA26PQvk2IbSN/rRgVbvlLTNgX2tzz1dyUA==} + + '@cosmjs/amino@0.32.4': + resolution: {integrity: sha512-zKYOt6hPy8obIFtLie/xtygCkH9ZROiQ12UHfKsOkWaZfPQUvVbtgmu6R4Kn1tFLI/SRkw7eqhaogmW/3NYu/Q==} + + '@cosmjs/amino@0.37.0': + resolution: {integrity: sha512-Qjg3vx0V907ICYr9wTFuF55+P2F7FuVuVdV8WsBJC6G9ekS5nbi7Z4+YsoJf3JEp5WApVgcX3HmAWZhziayxzw==} + + '@cosmjs/cosmwasm-stargate@0.37.0': + resolution: {integrity: sha512-q3bezZzAIyPpUT89CpQrOhI6xJU4wiMU0ufN5nsPtRaZ1Cn8mwoj76j56wb7lvwsAPIk7ALjVbQwULmn7hii6A==} + + '@cosmjs/crypto@0.32.4': + resolution: {integrity: sha512-zicjGU051LF1V9v7bp8p7ovq+VyC91xlaHdsFOTo2oVry3KQikp8L/81RkXmUIT8FxMwdx1T7DmFwVQikcSDIw==} + deprecated: This uses elliptic for cryptographic operations, which contains several security-relevant bugs. To what degree this affects your application is something you need to carefully investigate. See https://github.com/cosmos/cosmjs/issues/1708 for further pointers. Starting with version 0.34.0 the cryptographic library has been replaced. However, private keys might still be at risk. + + '@cosmjs/crypto@0.37.0': + resolution: {integrity: sha512-rjnU7SEgNTUQAUotG686m7ahYSWgHh3J6n2JXoWoHJz0uVv4o4P+pbAFklyQ1PcPIR7u6LezCKDB5tP5Y5PeYQ==} + + '@cosmjs/encoding@0.32.4': + resolution: {integrity: sha512-tjvaEy6ZGxJchiizzTn7HVRiyTg1i4CObRRaTRPknm5EalE13SV+TCHq38gIDfyUeden4fCuaBVEdBR5+ti7Hw==} + + '@cosmjs/encoding@0.37.0': + resolution: {integrity: sha512-xtdC0w+iVFOrod9a5RLJULUECv+6AvZr5FkD8AFr2vD853n7Z89/AVuEiJzd4GdUwlPzxcaamhAtmI+IB9DYvg==} + + '@cosmjs/json-rpc@0.32.4': + resolution: {integrity: sha512-/jt4mBl7nYzfJ2J/VJ+r19c92mUKF0Lt0JxM3MXEJl7wlwW5haHAWtzRujHkyYMXOwIR+gBqT2S0vntXVBRyhQ==} + + '@cosmjs/json-rpc@0.37.0': + resolution: {integrity: sha512-MzuVu2hUmY6uOhn2ezoe9AhR/WBIKYYWarr8QazVnM2N5gZQnfL3Pg/i+f2f4lhacSwYVnJXfqF1CZJoDi0AUg==} + + '@cosmjs/math@0.32.4': + resolution: {integrity: sha512-++dqq2TJkoB8zsPVYCvrt88oJWsy1vMOuSOKcdlnXuOA/ASheTJuYy4+oZlTQ3Fr8eALDLGGPhJI02W2HyAQaw==} + + '@cosmjs/math@0.37.0': + resolution: {integrity: sha512-FI+Tq8mhW0tuDawRvPdyX3K7qDZD2v1keRhiK/zHisvtQVzqoRRoOS1g5P9Pc7gWLQ1jPS15gDMYBu5+UYJ1+g==} + + '@cosmjs/proto-signing@0.32.3': + resolution: {integrity: sha512-kSZ0ZUY0DwcRT0NcIn2HkadH4NKlwjfZgbLj1ABwh/4l0RgeT84QCscZCu63tJYq3K6auwqTiZSZERwlO4/nbg==} + + '@cosmjs/proto-signing@0.37.0': + resolution: {integrity: sha512-Ir/nPyrKIlFKsNAfUPVAfgNj0NBEHyryzIPJbCebTK9fBuAyQ3LXn+QfiFYR58/kVp63gEdIlADMQGGBS5ZF+w==} + + '@cosmjs/socket@0.32.4': + resolution: {integrity: sha512-davcyYziBhkzfXQTu1l5NrpDYv0K9GekZCC9apBRvL1dvMc9F/ygM7iemHjUA+z8tJkxKxrt/YPjJ6XNHzLrkw==} + + '@cosmjs/socket@0.37.0': + resolution: {integrity: sha512-v07ufhrGXgja8O7ZTkUn66WJ2uWjtnH6F4KFNxI1XayrrHpVExlKS7BQIr5pKY1XUkKHCIcahuN4sRiPOOE38Q==} + + '@cosmjs/stargate@0.32.3': + resolution: {integrity: sha512-OQWzO9YWKerUinPIxrO1MARbe84XkeXJAW0lyMIjXIEikajuXZ+PwftiKA5yA+8OyditVmHVLtPud6Pjna2s5w==} + + '@cosmjs/stargate@0.37.0': + resolution: {integrity: sha512-accDRW9QvMbWf6zrg/YHR/E8UCDr1q2gu8zUI7Hdn8KjhO4yO/+Ry119Lqr8FRNxCJ4DvLe8yhYDLk5FFKuQCQ==} + + '@cosmjs/stream@0.32.4': + resolution: {integrity: sha512-Gih++NYHEiP+oyD4jNEUxU9antoC0pFSg+33Hpp0JlHwH0wXhtD3OOKnzSfDB7OIoEbrzLJUpEjOgpCp5Z+W3A==} + + '@cosmjs/stream@0.37.0': + resolution: {integrity: sha512-xMfNL6GNCe08erwEJ6W7Oj34VqdCmCTyB/d68DIjgcXczOxndNHsldKyZLcLQse7jbwtDegYY68hW1OqikwWwg==} + + '@cosmjs/tendermint-rpc@0.32.3': + resolution: {integrity: sha512-xeprW+VR9xKGstqZg0H/KBZoUp8/FfFyS9ljIUTLM/UINjP2MhiwncANPS2KScfJVepGufUKk0/phHUeIBSEkw==} + + '@cosmjs/tendermint-rpc@0.32.4': + resolution: {integrity: sha512-MWvUUno+4bCb/LmlMIErLypXxy7ckUuzEmpufYYYd9wgbdCXaTaO08SZzyFM5PI8UJ/0S2AmUrgWhldlbxO8mw==} + + '@cosmjs/tendermint-rpc@0.37.0': + resolution: {integrity: sha512-TTLJwiA9FLLrQ7OIJxo2vt+HoTW54IUaBx7tptOvwCilYFQDNWYQaC22ACLF9sOPx3PtDqDpDDqDHfRbODD1sw==} + + '@cosmjs/utils@0.32.4': + resolution: {integrity: sha512-D1Yc+Zy8oL/hkUkFUL/bwxvuDBzRGpc4cF7/SkdhxX4iHpSLgdOuTt1mhCh9+kl6NQREy9t7SYZ6xeW5gFe60w==} + + '@cosmjs/utils@0.37.0': + resolution: {integrity: sha512-j46yZg+cLBpANGc5sCxtQHJgPGBt5zSKlU+KX9FlDS6JU6q2vZYCpKgucFFpjekEiMagU1eu8cDSCRjTthEM6w==} + + '@cosmology/lcd@0.13.5': + resolution: {integrity: sha512-CI8KFsJcgp0RINF8wHpv3Y9yR4Fb9ZnGucyoUICjtX2XT4NVBK+fvZuRFj5TP34km8TpEOb+WV2T7IN/pZsD7Q==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1463,6 +1594,12 @@ packages: '@solana/spl-token': ^0.4.12 '@solana/web3.js': ^1.90.0 + '@osmonauts/math@1.18.0': + resolution: {integrity: sha512-LqWjX8SgkudkQzUe1PosaclUOOE3DxygWwuUihaKcpd7UbRmeEy4ZDFbaFMfUmTQAiKxeec+4SLz/98x08ZTtA==} + + '@osmonauts/utils@1.18.0': + resolution: {integrity: sha512-OygY8qV7FTyrZu6AWbmSidyIZ88j1K7viELU6KFDEYiyjNGGKxjj8ghxb083pL2mq/MnkPP9dYKjeKRQ1E6mYg==} + '@pancakeswap/chains@0.5.1': resolution: {integrity: sha512-wIYRrC7iQfd/ILe4/SJ6nQ+GZmHcy3ylKYH8O8Thfd1WmbJ9G4qnQkL0wJBmA2NUkMO/qceWQAthr2BCosl/ZA==} engines: {node: '>=10'} @@ -1574,6 +1711,36 @@ packages: resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@randlabs/communication-bridge@1.0.1': resolution: {integrity: sha512-CzS0U8IFfXNK7QaJFE4pjbxDGfPjbXBEsEaCn9FN15F+ouSAEUQkva3Gl66hrkBZOGexKFEWMwUHIDKpZ2hfVg==} @@ -1592,6 +1759,9 @@ packages: '@scure/base@1.2.5': resolution: {integrity: sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw==} + '@scure/base@2.0.0': + resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} + '@scure/bip32@1.1.5': resolution: {integrity: sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==} @@ -2147,6 +2317,9 @@ packages: '@types/levelup@5.1.5': resolution: {integrity: sha512-Sm0jSj+LoncQ8BuZZJBjYitY5r9/V/Xd//vRjfgbQLWcQg2/iCm0HQqIOZ1KBE7QdNyAqMIG97mE3+t1GR0TIw==} + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + '@types/lru-cache@5.1.1': resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} @@ -2836,8 +3009,11 @@ packages: resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==} engines: {node: '>= 10.0.0'} - bignumber.js@9.3.0: - resolution: {integrity: sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==} + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} @@ -3024,6 +3200,9 @@ packages: resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} engines: {node: '>=4'} + chain-registry@1.69.400: + resolution: {integrity: sha512-P0wldnb8kKCOwRqHbSUrrsgwaElYy05fvdYCUxvdiAfeWb1vD+WBf+C+gTHplR/mEtPbOLecGkZ5qQhBnhC9tg==} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3238,6 +3417,12 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cosmjs-types@0.10.1: + resolution: {integrity: sha512-CENXb4O5GN+VyB68HYXFT2SOhv126Z59631rZC56m8uMWa6/cSlFeai8BwZGT1NMepw0Ecf+U8XSOnBzZUWh9Q==} + + cosmjs-types@0.9.0: + resolution: {integrity: sha512-MN/yUe6mkJwHnCFfsNPeCfXVhyxHYW6c/xDUzrSbBycYzw++XvWDMJArXp2pLdgD6FQ8DW79vkPjeNKVrXaHeQ==} + create-hash@1.1.3: resolution: {integrity: sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==} @@ -4282,6 +4467,9 @@ packages: resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} engines: {node: '>=4'} + hash-wasm@4.12.0: + resolution: {integrity: sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==} + hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} @@ -4956,6 +5144,12 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libsodium-sumo@0.7.15: + resolution: {integrity: sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw==} + + libsodium-wrappers-sumo@0.7.15: + resolution: {integrity: sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==} + light-my-request@5.14.0: resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} @@ -5010,6 +5204,9 @@ packages: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} + long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -5409,6 +5606,12 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + osmo-query@16.14.0: + resolution: {integrity: sha512-r9aQ7fmjaa9kAY9kZAROkcHaILeHzZ1XximS0LsqYKKCY/th1ZGYme8TYh0JLAOqBVnW8yDP2coV2YlVFh65+A==} + + osmojs@16.15.0: + resolution: {integrity: sha512-ERIXRzSF+EkS+RNFSzhTurr/EfWnpNfV6b1onf0MXd+YA3X3t8WbkboXg9+/ol61HDEGjugEGzRtz6sFvwaC3w==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -5612,6 +5815,10 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + protobufjs@6.11.4: + resolution: {integrity: sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==} + hasBin: true + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5700,6 +5907,9 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + readonly-date@1.0.0: + resolution: {integrity: sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -6220,6 +6430,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + symbol-observable@2.0.3: + resolution: {integrity: sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==} + engines: {node: '>=0.10'} + synckit@0.11.4: resolution: {integrity: sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -6730,6 +6944,9 @@ packages: utf-8-validate: optional: true + xstream@11.14.0: + resolution: {integrity: sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -7457,7 +7674,7 @@ snapshots: async-retry: 1.3.3 axios: 1.9.0(debug@4.4.0) base64url: 3.0.1 - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 bs58: 4.0.1 commander: 8.3.0 csv: 6.3.11 @@ -7474,8 +7691,23 @@ snapshots: - typescript - utf-8-validate + '@chain-registry/types@0.50.255': {} + + '@chain-registry/types@2.0.104': {} + + '@chain-registry/utils@1.51.255': + dependencies: + '@chain-registry/types': 0.50.255 + bignumber.js: 9.1.2 + sha.js: 2.4.11 + '@colors/colors@1.6.0': {} + '@confio/ics23@0.6.8': + dependencies: + '@noble/hashes': 1.8.0 + protobufjs: 6.11.4 + '@coral-xyz/anchor-errors@0.30.1': {} '@coral-xyz/anchor-errors@0.31.1': {} @@ -7536,6 +7768,227 @@ snapshots: bn.js: 5.2.1 buffer-layout: 1.2.2 + '@cosmjs/amino@0.32.3': + dependencies: + '@cosmjs/crypto': 0.32.4 + '@cosmjs/encoding': 0.32.4 + '@cosmjs/math': 0.32.4 + '@cosmjs/utils': 0.32.4 + + '@cosmjs/amino@0.32.4': + dependencies: + '@cosmjs/crypto': 0.32.4 + '@cosmjs/encoding': 0.32.4 + '@cosmjs/math': 0.32.4 + '@cosmjs/utils': 0.32.4 + + '@cosmjs/amino@0.37.0': + dependencies: + '@cosmjs/crypto': 0.37.0 + '@cosmjs/encoding': 0.37.0 + '@cosmjs/math': 0.37.0 + '@cosmjs/utils': 0.37.0 + + '@cosmjs/cosmwasm-stargate@0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/amino': 0.37.0 + '@cosmjs/crypto': 0.37.0 + '@cosmjs/encoding': 0.37.0 + '@cosmjs/math': 0.37.0 + '@cosmjs/proto-signing': 0.37.0 + '@cosmjs/stargate': 0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/tendermint-rpc': 0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/utils': 0.37.0 + cosmjs-types: 0.10.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@cosmjs/crypto@0.32.4': + dependencies: + '@cosmjs/encoding': 0.32.4 + '@cosmjs/math': 0.32.4 + '@cosmjs/utils': 0.32.4 + '@noble/hashes': 1.8.0 + bn.js: 5.2.1 + elliptic: 6.6.1 + libsodium-wrappers-sumo: 0.7.15 + + '@cosmjs/crypto@0.37.0': + dependencies: + '@cosmjs/encoding': 0.37.0 + '@cosmjs/math': 0.37.0 + '@cosmjs/utils': 0.37.0 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.2 + '@noble/hashes': 1.8.0 + '@scure/bip39': 1.6.0 + hash-wasm: 4.12.0 + + '@cosmjs/encoding@0.32.4': + dependencies: + base64-js: 1.5.1 + bech32: 1.1.4 + readonly-date: 1.0.0 + + '@cosmjs/encoding@0.37.0': + dependencies: + '@scure/base': 2.0.0 + base64-js: 1.5.1 + readonly-date: 1.0.0 + + '@cosmjs/json-rpc@0.32.4': + dependencies: + '@cosmjs/stream': 0.32.4 + xstream: 11.14.0 + + '@cosmjs/json-rpc@0.37.0': + dependencies: + '@cosmjs/stream': 0.37.0 + xstream: 11.14.0 + + '@cosmjs/math@0.32.4': + dependencies: + bn.js: 5.2.1 + + '@cosmjs/math@0.37.0': {} + + '@cosmjs/proto-signing@0.32.3': + dependencies: + '@cosmjs/amino': 0.32.4 + '@cosmjs/crypto': 0.32.4 + '@cosmjs/encoding': 0.32.4 + '@cosmjs/math': 0.32.4 + '@cosmjs/utils': 0.32.4 + cosmjs-types: 0.9.0 + + '@cosmjs/proto-signing@0.37.0': + dependencies: + '@cosmjs/amino': 0.37.0 + '@cosmjs/crypto': 0.37.0 + '@cosmjs/encoding': 0.37.0 + '@cosmjs/math': 0.37.0 + '@cosmjs/utils': 0.37.0 + cosmjs-types: 0.10.1 + + '@cosmjs/socket@0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/stream': 0.32.4 + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@cosmjs/socket@0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/stream': 0.37.0 + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@cosmjs/stargate@0.32.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@confio/ics23': 0.6.8 + '@cosmjs/amino': 0.32.3 + '@cosmjs/encoding': 0.32.4 + '@cosmjs/math': 0.32.4 + '@cosmjs/proto-signing': 0.32.3 + '@cosmjs/stream': 0.32.4 + '@cosmjs/tendermint-rpc': 0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/utils': 0.32.4 + cosmjs-types: 0.9.0 + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@cosmjs/stargate@0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/amino': 0.37.0 + '@cosmjs/encoding': 0.37.0 + '@cosmjs/math': 0.37.0 + '@cosmjs/proto-signing': 0.37.0 + '@cosmjs/stream': 0.37.0 + '@cosmjs/tendermint-rpc': 0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/utils': 0.37.0 + cosmjs-types: 0.10.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@cosmjs/stream@0.32.4': + dependencies: + xstream: 11.14.0 + + '@cosmjs/stream@0.37.0': + dependencies: + xstream: 11.14.0 + + '@cosmjs/tendermint-rpc@0.32.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/crypto': 0.32.4 + '@cosmjs/encoding': 0.32.4 + '@cosmjs/json-rpc': 0.32.4 + '@cosmjs/math': 0.32.4 + '@cosmjs/socket': 0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/stream': 0.32.4 + '@cosmjs/utils': 0.32.4 + axios: 1.9.0 + readonly-date: 1.0.0 + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@cosmjs/tendermint-rpc@0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/crypto': 0.32.4 + '@cosmjs/encoding': 0.32.4 + '@cosmjs/json-rpc': 0.32.4 + '@cosmjs/math': 0.32.4 + '@cosmjs/socket': 0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/stream': 0.32.4 + '@cosmjs/utils': 0.32.4 + axios: 1.9.0 + readonly-date: 1.0.0 + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@cosmjs/tendermint-rpc@0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/crypto': 0.37.0 + '@cosmjs/encoding': 0.37.0 + '@cosmjs/json-rpc': 0.37.0 + '@cosmjs/math': 0.37.0 + '@cosmjs/socket': 0.37.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/stream': 0.37.0 + '@cosmjs/utils': 0.37.0 + readonly-date: 1.0.0 + xstream: 11.14.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@cosmjs/utils@0.32.4': {} + + '@cosmjs/utils@0.37.0': {} + + '@cosmology/lcd@0.13.5': + dependencies: + axios: 1.9.0 + transitivePeerDependencies: + - debug + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -8447,7 +8900,7 @@ snapshots: '@ledgerhq/logs': 6.13.0 '@ledgerhq/types-live': 6.76.0 axios: 1.9.0 - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 semver: 7.7.1 transitivePeerDependencies: - debug @@ -8509,7 +8962,7 @@ snapshots: '@ledgerhq/types-live@6.76.0': dependencies: - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 rxjs: 7.8.2 '@lukeed/ms@2.0.2': {} @@ -8598,7 +9051,7 @@ snapshots: '@solana/spl-token': 0.2.0(bufferutil@4.0.9)(encoding@0.1.13)(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) abort-controller: 3.0.0 - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 bn.js: 5.2.1 bs58: 5.0.0 buffer: 6.0.3 @@ -8812,6 +9265,28 @@ snapshots: decimal.js: 10.5.0 tiny-invariant: 1.3.3 + '@osmonauts/math@1.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@chain-registry/types': 0.50.255 + '@chain-registry/utils': 1.51.255 + bignumber.js: 9.1.2 + chain-registry: 1.69.400 + decimal.js-light: 2.5.1 + osmojs: 16.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@osmonauts/utils@1.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@cosmjs/amino': 0.32.3 + '@cosmjs/stargate': 0.32.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + '@pancakeswap/chains@0.5.1': {} '@pancakeswap/chains@0.6.0': {} @@ -8822,7 +9297,7 @@ snapshots: '@pancakeswap/permit2-sdk': 1.1.5(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4) '@pancakeswap/swap-sdk-core': 1.5.0 '@pancakeswap/v3-sdk': 3.9.5(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4) - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 tiny-invariant: 1.3.3 viem: 2.31.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.24.4) transitivePeerDependencies: @@ -8839,7 +9314,7 @@ snapshots: '@pancakeswap/permit2-sdk': 1.1.5(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@pancakeswap/swap-sdk-core': 1.5.0 '@pancakeswap/v3-sdk': 3.9.5(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 tiny-invariant: 1.3.3 viem: 2.31.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: @@ -9243,6 +9718,29 @@ snapshots: '@pkgr/core@0.2.4': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@randlabs/communication-bridge@1.0.1': {} '@randlabs/myalgo-connect@1.4.2': @@ -9277,6 +9775,8 @@ snapshots: '@scure/base@1.2.5': {} + '@scure/base@2.0.0': {} + '@scure/bip32@1.1.5': dependencies: '@noble/hashes': 1.2.0 @@ -9715,7 +10215,7 @@ snapshots: '@solana/buffer-layout': 4.0.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) bigint-buffer: 1.1.5 - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 transitivePeerDependencies: - bufferutil - encoding @@ -10159,6 +10659,8 @@ snapshots: '@types/level-errors': 3.0.2 '@types/node': 15.14.9 + '@types/long@4.0.2': {} + '@types/lru-cache@5.1.1': {} '@types/mathjs@9.4.2': @@ -10448,7 +10950,7 @@ snapshots: '@uniswap/v3-core': 1.0.0 '@uniswap/v3-sdk': 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 ethers: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil @@ -10467,7 +10969,7 @@ snapshots: '@uniswap/v3-core': 1.0.0 '@uniswap/v3-sdk': 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@15.14.9)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 ethers: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil @@ -10864,7 +11366,7 @@ snapshots: arconnect: 0.4.2 asn1.js: 5.4.1 base64-js: 1.5.1 - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 asn1.js@5.4.1: dependencies: @@ -11027,7 +11529,9 @@ snapshots: dependencies: bindings: 1.5.0 - bignumber.js@9.3.0: {} + bignumber.js@9.1.2: {} + + bignumber.js@9.3.1: {} binary-extensions@2.3.0: {} @@ -11280,6 +11784,10 @@ snapshots: pathval: 1.1.1 type-detect: 4.1.0 + chain-registry@1.69.400: + dependencies: + '@chain-registry/types': 0.50.255 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -11476,6 +11984,10 @@ snapshots: core-util-is@1.0.3: {} + cosmjs-types@0.10.1: {} + + cosmjs-types@0.9.0: {} + create-hash@1.1.3: dependencies: cipher-base: 1.0.6 @@ -12815,6 +13327,8 @@ snapshots: readable-stream: 3.6.2 safe-buffer: 5.2.1 + hash-wasm@4.12.0: {} + hash.js@1.1.7: dependencies: inherits: 2.0.4 @@ -13619,7 +14133,7 @@ snapshots: json-bigint@1.0.0: dependencies: - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 json-buffer@3.0.1: {} @@ -13709,6 +14223,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libsodium-sumo@0.7.15: {} + + libsodium-wrappers-sumo@0.7.15: + dependencies: + libsodium-sumo: 0.7.15 + light-my-request@5.14.0: dependencies: cookie: 0.7.2 @@ -13783,6 +14303,8 @@ snapshots: safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 + long@4.0.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -13853,7 +14375,7 @@ snapshots: merkletreejs@0.3.11: dependencies: - bignumber.js: 9.3.0 + bignumber.js: 9.3.1 buffer-reverse: 1.0.1 crypto-js: 4.2.0 treeify: 1.1.0 @@ -14223,6 +14745,30 @@ snapshots: os-tmpdir@1.0.2: {} + osmo-query@16.14.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@cosmjs/amino': 0.32.3 + '@cosmjs/proto-signing': 0.32.3 + '@cosmjs/stargate': 0.32.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/tendermint-rpc': 0.32.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmology/lcd': 0.13.5 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + osmojs@16.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@cosmjs/amino': 0.32.3 + '@cosmjs/proto-signing': 0.32.3 + '@cosmjs/stargate': 0.32.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmjs/tendermint-rpc': 0.32.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@cosmology/lcd': 0.13.5 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -14462,6 +15008,22 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + protobufjs@6.11.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/long': 4.0.2 + '@types/node': 15.14.9 + long: 4.0.0 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -14563,6 +15125,8 @@ snapshots: readdirp@4.1.2: {} + readonly-date@1.0.0: {} + real-require@0.2.0: {} reflect.getprototypeof@1.0.10: @@ -15143,6 +15707,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + symbol-observable@2.0.3: {} + synckit@0.11.4: dependencies: '@pkgr/core': 0.2.4 @@ -15736,6 +16302,11 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 + xstream@11.14.0: + dependencies: + globalthis: 1.0.4 + symbol-observable: 2.0.3 + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/src/app.ts b/src/app.ts index a4c69cd1a2..81e0a760f9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,11 +1,10 @@ // External dependencies -import { spawn } from 'child_process'; -import { exec } from 'child_process'; +import { spawn, exec } from 'child_process'; import { promisify } from 'util'; -import fastifyRateLimit from '@fastify/rate-limit'; -import fastifySwagger from '@fastify/swagger'; -import fastifySwaggerUi from '@fastify/swagger-ui'; +import { fastifyRateLimit } from '@fastify/rate-limit'; +import { fastifySwagger } from '@fastify/swagger'; +import { fastifySwaggerUi } from '@fastify/swagger-ui'; import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; import { Type } from '@sinclair/typebox'; import Fastify, { FastifyInstance } from 'fastify'; @@ -19,6 +18,8 @@ import { configRoutes } from './config/config.routes'; import { register0xRoutes } from './connectors/0x/0x.routes'; import { jupiterRoutes } from './connectors/jupiter/jupiter.routes'; import { meteoraRoutes } from './connectors/meteora/meteora.routes'; +import { osmosisChainRoutes } from './connectors/osmosis/chain-routes'; +import { osmosisRoutes } from './connectors/osmosis/osmosis.routes'; import { pancakeswapRoutes } from './connectors/pancakeswap/pancakeswap.routes'; import { pancakeswapSolRoutes } from './connectors/pancakeswap-sol/pancakeswap-sol.routes'; import { raydiumRoutes } from './connectors/raydium/raydium.routes'; @@ -27,7 +28,6 @@ import { getHttpsOptions } from './https'; import { poolRoutes } from './pools/pools.routes'; import { ConfigManagerV2 } from './services/config-manager-v2'; import { logger } from './services/logger'; -import { quoteCache } from './services/quote-cache'; import { displayChainConfigurations } from './services/startup-banner'; import { tokensRoutes } from './tokens/tokens.routes'; import { tradingRoutes, tradingClmmRoutes } from './trading/trading.routes'; @@ -76,6 +76,10 @@ const swaggerOptions = { name: '/chain/ethereum', description: 'Ethereum and EVM-based chain endpoints', }, + { + name: '/chain/cosmos', + description: 'Cosmos (via Osmosis RPC) chain endpoints', + }, // Connectors { @@ -103,6 +107,10 @@ const swaggerOptions = { name: '/connector/pancakeswap', description: 'PancakeSwap EVM connector endpoints', }, + { + name: '/connector/osmosis', + description: 'Osmosis connector endpoints', + }, ], components: { parameters: { @@ -235,6 +243,7 @@ const configureGatewayServer = () => { // Register chain routes app.register(solanaRoutes, { prefix: '/chains/solana' }); app.register(ethereumRoutes, { prefix: '/chains/ethereum' }); + app.register(osmosisChainRoutes, { prefix: '/chains/cosmos' }); // Register DEX connector routes - organized by connector @@ -269,6 +278,9 @@ const configureGatewayServer = () => { // PancakeSwap Solana routes app.register(pancakeswapSolRoutes, { prefix: '/connectors/pancakeswap-sol' }); + + app.register(osmosisRoutes.amm, { prefix: '/connectors/osmosis/amm' }); + app.register(osmosisRoutes.clmm, { prefix: '/connectors/osmosis/clmm' }); }; // Register routes on main server diff --git a/src/chains/cosmos/cosmos-base.ts b/src/chains/cosmos/cosmos-base.ts new file mode 100755 index 0000000000..f2ff0094ca --- /dev/null +++ b/src/chains/cosmos/cosmos-base.ts @@ -0,0 +1,457 @@ +import crypto from 'crypto'; +import { promises as fs } from 'fs'; + +import { fromHex } from '@cosmjs/encoding'; +import { DirectSecp256k1Wallet, AccountData } from '@cosmjs/proto-signing'; +import { StargateClient, setupIbcExtension } from '@cosmjs/stargate'; +import axios from 'axios'; +import { BigNumber } from 'bignumber.js'; +import fse from 'fs-extra'; +import { osmosis } from 'osmojs'; + +import { SerializableExtendedPool as CosmosSerializableExtendedPool } from '../../connectors/osmosis/osmosis.types'; // CosmosSerializableExtendedPool is a custom type for extended pool info +import { MarketListType as tokenListType, stringInsert } from '../../services/base'; +import { ConfigManagerCertPassphrase } from '../../services/config-manager-cert-passphrase'; +import { logger } from '../../services/logger'; +import { TokenService } from '../../services/token-service'; +import { getSafeWalletFilePath, isHardwareWallet as isHardwareWalletUtil } from '../../wallet/utils'; +import { getEIP1559DynamicBaseFee } from '../cosmos/cosmos.prices'; + +import { CosmosAsset, AssetList } from './cosmos.universaltypes'; +import { isValidCosmosAddress } from './cosmos.validators'; +const { createRPCQueryClient } = osmosis.ClientFactory; + +// a nice way to represent the token value without carrying around as a string +export interface CosmosTokenValue { + value: BigNumber; + decimals: number; +} + +export interface TokensJson { + assets: CosmosAsset[]; +} + +export interface PositionInfo { + token0?: string | undefined; + token1?: string | undefined; + poolShares?: string; // COSMOS - GAMM pools only issue poolShares (no amount/unclaimedToken) + fee?: string | undefined; + lowerPrice?: string; + upperPrice?: string; + amount0?: string; // COSMOS - CL pools only + amount1?: string; // COSMOS - CL pools only + unclaimedToken0?: string; // COSMOS - CL pools only + unclaimedToken1?: string; // COSMOS - CL pools only + pools?: CosmosSerializableExtendedPool[]; +} + +export class CosmosWallet { + member: DirectSecp256k1Wallet; + pubkey: Uint8Array; + privkey: Uint8Array; + prefix: string; + address: string; +} +export async function cWalletMaker(privkey: Uint8Array, prefix: string): Promise { + const member = await DirectSecp256k1Wallet.fromKey(privkey, prefix); + const wallet = new CosmosWallet(); + wallet.member = member; + wallet.privkey = privkey; + wallet.prefix = prefix; + wallet.pubkey = (await member + .getAccounts() + .then((accounts: readonly AccountData[]) => accounts[0].pubkey)) as Uint8Array; + wallet.address = await member.getAccounts().then((accounts: readonly AccountData[]) => accounts[0].address); + return wallet; +} + +export interface KeyAlgorithm { + name: string; + salt: Uint8Array; + iterations: number; + hash: string; +} + +export interface CipherAlgorithm { + name: string; + iv: Uint8Array; +} +export interface EncryptedPrivateKey { + keyAlgorithm: KeyAlgorithm; + cipherAlgorithm: CipherAlgorithm; + ciphertext: Uint8Array; +} + +export type NewBlockHandler = (bn: number) => void; + +export type NewDebugMsgHandler = (msg: any) => void; + +// convert a BigNumber and the number of decimals into a numeric string. +// this makes it JavaScript compatible while preserving all the data. +export const bigNumberWithDecimalToStr = (n: BigNumber, d: number): string => { + const n_ = n.toString(); + + let zeros = ''; + + if (n_.length <= d) { + zeros = '0'.repeat(d - n_.length + 1); + } + + return stringInsert(n_.split('').reverse().join('') + zeros, '.', d) + .split('') + .reverse() + .join(''); +}; + +// we should turn Token into a string when we return as a value in an API call +export const tokenValueToString = (t: CosmosTokenValue | string): string => { + return typeof t === 'string' ? t : bigNumberWithDecimalToStr(t.value, t.decimals); +}; + +export class CosmosBase { + public _provider: any = undefined; + protected tokenList: CosmosAsset[] = []; + protected _tokenMap: Record = {}; + + public assetList: AssetList[] = []; + public _ready: boolean = false; + public _initialized: Promise = Promise.resolve(false); + + public network; + public chainName: string; + public rpcProvider: string; + public nodeURL; + public gasAdjustment; + public manualGasPrice: number; + public allowedSlippage: string; + public gasLimitTransaction: number; + public manualGasPriceToken: string; + public feeTier: string; + public rpcAddressDynamicBaseFee: string; + public useEIP1559DynamicBaseFeeInsteadOfManualGasPrice: boolean; + public lastBaseFee: number; + + constructor( + network: string, + chainName: string, + nodeURL: string, + gasAdjustment: number, // adjustment + feeTier: string, + manualGasPriceToken: string, + gasLimitTransaction: number, + allowedSlippage: string, + rpcProvider: string, + useEIP1559DynamicBaseFeeInsteadOfManualGasPrice?: boolean, + rpcAddressDynamicBaseFee?: string, + manualGasPrice?: number, + ) { + this.network = network; + this.chainName = chainName; + this.feeTier = feeTier; + this.manualGasPriceToken = manualGasPriceToken; + this.gasLimitTransaction = gasLimitTransaction; + this.allowedSlippage = allowedSlippage; + this.manualGasPrice = manualGasPrice!; + this.nodeURL = nodeURL; + this.gasAdjustment = gasAdjustment; + this.useEIP1559DynamicBaseFeeInsteadOfManualGasPrice = useEIP1559DynamicBaseFeeInsteadOfManualGasPrice!; + this.rpcAddressDynamicBaseFee = rpcAddressDynamicBaseFee!; + this.rpcProvider = rpcProvider; + } + + ready(): boolean { + return this._ready; + } + + public get provider() { + return this._provider; + } + + async init(): Promise { + await this._initialized; // Wait for any previous init() calls to complete + if (!this.ready()) { + if (this.chainName == 'cosmos') { + this._provider = await StargateClient.connect(this.nodeURL); + } else { + // osmosis + this._provider = await createRPCQueryClient({ rpcEndpoint: this.nodeURL }); + } + await this.getLatestBasePrice(); + // If we're not ready, this._initialized will be a Promise that resolves after init() completes + this._initialized = (async () => { + try { + if (this.chainName) { + await this.loadTokens(); + } + return true; + } catch (e) { + logger.error(`Failed to initialize ${this.chainName} chain: ${e}`); + return false; + } + })(); + this._ready = await this._initialized; // Wait for the initialization to complete + } + return; + } + + async getLatestBasePrice(): Promise { + if (this.useEIP1559DynamicBaseFeeInsteadOfManualGasPrice) { + const eipPrice = await getEIP1559DynamicBaseFee(this.rpcAddressDynamicBaseFee); + if (eipPrice != '') { + this.manualGasPrice = Number(eipPrice); + } + } + return this.manualGasPrice ? this.manualGasPrice : 0.025; + } + + /** + * Load tokens from the token list source + */ + public async loadTokens(): Promise { + logger.info(`Loading tokens for Cosmos-Osmosis/${this.network} using TokenService`); + try { + // Use TokenService to load tokens + const tokens = await TokenService.getInstance().loadTokenList('cosmos', this.network); + + // Convert to TokenInfo format with chainId and fake addresses + this.tokenList = []; + tokens.forEach((token) => { + if (['osmosistestnet', 'osmosis'].includes(token.chainName)) { + this.tokenList.push(new CosmosAsset(token)); + } + }); + if (this.tokenList) { + logger.info(`Loaded ${this.tokenList.length} tokens for Cosmos-Osmosis/${this.network}`); + this.tokenList.forEach((token: CosmosAsset) => (this._tokenMap[token.symbol] = token)); + } + this.assetList = [ + { + chainName: 'osmosis', + chain_name: 'osmosis', + assets: this.tokenList, + }, + ]; + } catch (error) { + logger.error(`Failed to load token list: ${error.message}`); + throw error; + } + } + + // Original token list loading logic. Retained for URL JSON load support. + async getTokenList(tokenListSource: string, tokenListType: tokenListType): Promise { + const tokens: CosmosAsset[] = []; + let tokensJson: any; + + if (tokenListType === 'URL') { + ({ data: tokensJson } = await axios.get(tokenListSource)); + } else { + tokensJson = JSON.parse(await fs.readFile(tokenListSource, 'utf8')); + } + if (this.chainName == 'cosmos') { + // URL source is a bit different for cosmos + tokensJson = tokensJson as any[]; + tokensJson.forEach((tokenAsset) => { + const cosmosAssetInstance = new CosmosAsset(tokenAsset); + if (cosmosAssetInstance) { + cosmosAssetInstance.chainName = this.chainName; + tokens.push(cosmosAssetInstance); + } + }); + } else if (this.chainName == 'osmosis') { + tokensJson = tokensJson as TokensJson; + for (let tokenAssetIdx = 0; tokenAssetIdx < tokensJson.assets.length; tokenAssetIdx++) { + const tokenAsset = tokensJson.assets[tokenAssetIdx]; + const cosmosAssetInstance = new CosmosAsset(tokenAsset); + if (cosmosAssetInstance) { + cosmosAssetInstance.chainName = this.chainName; + tokens.push(cosmosAssetInstance); + } + } + } + + return tokens; + } + + // ethereum token lists are large. instead of reloading each time with + // getTokenList, we can read the stored tokenList value from when the + // object was initiated. + public get storedTokenList(): CosmosAsset[] { + return this.tokenList; + } + + // return the Token object for a symbol + getTokenForSymbol(symbol: string): CosmosAsset | null { + return this._tokenMap[symbol] ? this._tokenMap[symbol] : null; + } + + async getWalletFromPrivateKey(privateKey: string, prefix: string): Promise { + const cwallet = await cWalletMaker(fromHex(privateKey), prefix); + return cwallet; + } + + async getWallet(address: string, prefix: string): Promise { + try { + // Validate the address format first + const validatedAddress = isValidCosmosAddress(address); + + // Use the safe wallet file path utility to prevent path injection + const safeWalletPath = getSafeWalletFilePath('cosmos', validatedAddress); + + // Read the wallet file using the safe path + const encryptedPrivateKey: string = await fse.readFile(safeWalletPath, 'utf8'); + const passphrase = ConfigManagerCertPassphrase.readPassphrase(); + if (!passphrase) { + throw new Error('missing passphrase'); + } + const decrypted = await this.decrypt(encryptedPrivateKey, passphrase); + // const secretKeyBytes = new Uint8Array(bs58.decode(decrypted)); + return await this.getWalletFromPrivateKey(decrypted, prefix); + // return await this.getWalletFromPrivateKey(Buffer.from(secretKeyBytes).toString('hex'), prefix); + } catch (error) { + if (error.message.includes('Invalid Cosmos address')) { + throw new Error(`Invalid wallet address: ${address}`); + } + if (error.code === 'ENOENT') { + throw new Error(`Wallet not found for address: ${address}`); + } + throw error; + } + } + + /** + * Check if an address is a hardware wallet + */ + async isHardwareWallet(address: string): Promise { + try { + return await isHardwareWalletUtil('cosmos', address); + } catch (error) { + logger.error(`Error checking hardware wallet status: ${error.message}`); + return false; + } + } + + async encrypt(secret: string, password: string): Promise { + const algorithm = 'aes-256-ctr'; + const iv = crypto.randomBytes(16); + const salt = crypto.randomBytes(32); + const key = crypto.pbkdf2Sync(password, new Uint8Array(salt), 5000, 32, 'sha512'); + const cipher = crypto.createCipheriv(algorithm, new Uint8Array(key), new Uint8Array(iv)); + + const encryptedBuffers = [ + new Uint8Array(cipher.update(new Uint8Array(Buffer.from(secret)))), + new Uint8Array(cipher.final()), + ]; + const encrypted = Buffer.concat(encryptedBuffers); + + const ivJSON = iv.toJSON(); + const saltJSON = salt.toJSON(); + const encryptedJSON = encrypted.toJSON(); + + return JSON.stringify({ + algorithm, + iv: ivJSON, + salt: saltJSON, + encrypted: encryptedJSON, + }); + } + + async decrypt(encryptedSecret: string, password: string): Promise { + const hash = JSON.parse(encryptedSecret); + const salt = new Uint8Array(Buffer.from(hash.salt, 'utf8')); + const iv = new Uint8Array(Buffer.from(hash.iv, 'utf8')); + + const key = crypto.pbkdf2Sync(password, salt, 5000, 32, 'sha512'); + + const decipher = crypto.createDecipheriv(hash.algorithm, new Uint8Array(key), iv); + + const decryptedBuffers = [ + new Uint8Array(decipher.update(new Uint8Array(Buffer.from(hash.encrypted, 'hex')))), + new Uint8Array(decipher.final()), + ]; + const decrypted = Buffer.concat(decryptedBuffers); + + return decrypted.toString(); + } + + async getDenomMetadata(provider: any, denom: string): Promise { + return await provider.queryClient.bank.denomMetadata(denom); + } + + getTokenDecimals(token: any): number { + return token ? token.denomUnits[token.denomUnits.length - 1].exponent : 6; // Last denom unit has the decimal amount we need from our list + } + + async getBalances(wallet: CosmosWallet): Promise> { + const balances: Record = {}; + + const provider = await this._provider; + + const accounts = await wallet.member.getAccounts(); + + const { address } = accounts[0]; + + const allTokens = await provider.getAllBalances(address); + + await Promise.all( + allTokens.map(async (t: { denom: string; amount: string }) => { + let token = this.getTokenByBase(t.denom); + + if (!token && t.denom.startsWith('ibc/')) { + const ibcHash: string = t.denom.replace('ibc/', ''); + + // Get base denom by IBC hash + if (ibcHash) { + const { denomTrace } = await setupIbcExtension(await provider.queryClient).ibc.transfer.denomTrace(ibcHash); + + if (denomTrace) { + const { baseDenom } = denomTrace; + + token = this.getTokenByBase(baseDenom); + } + } + } + + // Not all tokens are added in the registry so we use the denom if the token doesn't exist + balances[token ? token.symbol : t.denom] = { + value: new BigNumber(parseInt(t.amount, 10)), + decimals: this.getTokenDecimals(token), + }; + }), + ); + + return balances; + } + + // returns a cosmos tx for a txHash + async getTransaction(id: string): Promise { + const provider = await this._provider; + const transaction = await provider.cosmos.tx.v1beta1.getTx({ hash: id }); + if (!transaction) { + throw new Error('Transaction not found'); + } + return transaction; + } + + public getTokenBySymbol(tokenSymbol: string): CosmosAsset | undefined { + return this.tokenList.find((token: CosmosAsset) => token.symbol.toUpperCase() === tokenSymbol.toUpperCase()); + } + + public getTokenByBase(base: string): CosmosAsset | undefined { + return this.tokenList.find((token: CosmosAsset) => token.base === base); + } + + // generic - by denom or symbol (since we don't have token address) + public getToken(tokenString: string): CosmosAsset | undefined { + if (this.getTokenByBase(tokenString)) { + return this.getTokenByBase(tokenString); + } else { + return this.tokenList.find((token: CosmosAsset) => token.symbol.toUpperCase() === tokenString.toUpperCase()); + } + } + + async getCurrentBlockNumber(): Promise { + const provider = await this._provider; + + return await provider.getHeight(); + } +} diff --git a/src/chains/cosmos/cosmos.config.ts b/src/chains/cosmos/cosmos.config.ts new file mode 100755 index 0000000000..c111487dcf --- /dev/null +++ b/src/chains/cosmos/cosmos.config.ts @@ -0,0 +1,60 @@ +import { ConfigManagerV2 } from '../../services/config-manager-v2'; +export interface NetworkConfig { + name: string; + nodeURL: string; + // DISABLED: + // tokenListType: URL + // tokenListSource: https://cosmos-chain-registry-list.vercel.app/list.json +} + +interface AvailableNetworks { + chain: string; + networks: Array; +} + +export interface Config { + network: NetworkConfig; + chainName: string; + nativeCurrencySymbol: string; + manualGasPriceToken: string; + gasAdjustment: number; + manualGasPrice: number; + gasLimitTransaction: number; + allowedSlippage: string; + feeTier: string; + useEIP1559DynamicBaseFeeInsteadOfManualGasPrice: boolean; + rpcAddressDynamicBaseFee: string; + defaultNetwork: string; + defaultWallet: string; + availableNetworks: Array; +} + +export function getCosmosConfig(network: string): Config { + const configManager = ConfigManagerV2.getInstance(); + return { + network: { + name: network, + nodeURL: configManager.get(network + '.networks.' + network + '.nodeURL'), + }, + chainName: configManager.get(network + '.networks.' + network + '.chainName'), + availableNetworks: [ + { + chain: 'osmosis', + networks: ['mainnet', 'testnet'], + }, + ], + nativeCurrencySymbol: configManager.get(network + '.nativeCurrencySymbol'), + manualGasPrice: configManager.get(network + '.manualGasPrice'), + gasLimitTransaction: configManager.get(network + '.gasLimitTransaction'), + manualGasPriceToken: configManager.get(network + '.manualGasPriceToken'), + gasAdjustment: configManager.get(network + '.gasAdjustment'), + allowedSlippage: configManager.get(network + '.allowedSlippage'), + feeTier: configManager.get(network + '.feeTier'), + useEIP1559DynamicBaseFeeInsteadOfManualGasPrice: configManager.get( + network + '.useEIP1559DynamicBaseFeeInsteadOfManualGasPrice', + ), + rpcAddressDynamicBaseFee: configManager.get(network + '.rpcAddressDynamicBaseFee'), + defaultNetwork: configManager.get(network + '.defaultNetwork'), + defaultWallet: configManager.get(network + '.defaultWallet'), + }; +} diff --git a/src/chains/cosmos/cosmos.controllers.ts b/src/chains/cosmos/cosmos.controllers.ts new file mode 100755 index 0000000000..82382e6ced --- /dev/null +++ b/src/chains/cosmos/cosmos.controllers.ts @@ -0,0 +1,34 @@ +import { decodeTxRaw } from '@cosmjs/proto-signing'; + +import { logger } from '../../services/logger'; + +import { Cosmos } from './cosmos'; +import { CosmosTokenValue, tokenValueToString } from './cosmos-base'; +import { CosmosBalanceRequest, CosmosPollRequest } from './cosmos.requests'; +import { CosmosAsset } from './cosmos.universaltypes'; + +export const toCosmosBalances = ( + balances: Record, + // tokenSymbols: Array, + tokenAssets: CosmosAsset[], + manualTokensCheck: boolean = false, // send 0's when they send in exact tokens to check +): Record => { + const walletBalances: Record = {}; + + Object.keys(balances).forEach((key) => { + const value: CosmosTokenValue = balances[key]; + const symbol = key; + let balance = 0; + const asset: CosmosAsset = tokenAssets.find((ast) => { + return [ast.symbol, ast.address, ast.base].includes(key); // filter out all by gamm/pool/ID values returned by balances() + }); + if (asset) { + key = asset.symbol; + } + balance = Number(tokenValueToString(value)); + if (balance > 0 || manualTokensCheck) { + walletBalances[symbol] = balance; + } + }); + return walletBalances; +}; diff --git a/src/chains/cosmos/cosmos.prices.ts b/src/chains/cosmos/cosmos.prices.ts new file mode 100755 index 0000000000..69953ba71c --- /dev/null +++ b/src/chains/cosmos/cosmos.prices.ts @@ -0,0 +1,127 @@ +import { PriceHash } from '@osmonauts/math/esm/types'; + +import { CosmosAsset } from '../../chains/cosmos/cosmos.universaltypes'; + +type CoinGeckoId = string; +type CoinGeckoUSD = { usd: number }; +type CoinGeckoUSDResponse = Record; + +const getAssetsWithGeckoIds = (assets: CosmosAsset[]) => { + return assets.filter((asset) => asset.coingeckoId); +}; + +const getGeckoIds = (assets: CosmosAsset[]) => { + return assets.map((asset) => asset.coingeckoId) as string[]; +}; + +const formatPrices = (prices: CoinGeckoUSDResponse, assets: CosmosAsset[]): Record => { + return Object.entries(prices).reduce((priceHash, cur) => { + const key = assets.find((asset) => asset.coingeckoId === cur[0])!.base; + // const key = assets.find((asset) => asset.coingeckoId === cur[0])!.symbol; // hash by symbol + return { ...priceHash, [key]: cur[1].usd }; + }, {}); +}; + +export const getCoinGeckoPrices = async (assets: CosmosAsset[]): Promise> => { + const assetsWithGeckoIds = getAssetsWithGeckoIds(assets); + const geckoIds = getGeckoIds(assetsWithGeckoIds); + + const priceData: CoinGeckoUSDResponse | undefined = await getData(geckoIds); + if (priceData) { + return formatPrices(priceData, assetsWithGeckoIds); + } + throw new Error('Osmosis failed to get prices from coingecko.com'); +}; + +const getData = async (geckoIds: string[]): Promise => { + let prices: CoinGeckoUSDResponse; + console.info('Osmosis: Getting coin prices from coingecko.'); + const url = `https://api.coingecko.com/api/v3/simple/price?ids=${geckoIds.join()}&vs_currencies=usd`; + try { + const response = await fetch(url); + if (!response.ok) { + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + prices = (await response.json()) as CoinGeckoUSDResponse; + if (!response.ok) { + throw Error('Get price error'); + } + return prices; + } catch (err) { + console.error(err); + } + return undefined; +}; + +export const getImperatorPriceHash = async ( + tokenList?: CosmosAsset[], + url: string = 'https://api-osmosis.imperator.co/tokens/v2/all', +) => { + let prices = []; + + try { + const response = await fetch(url); + if (!response.ok) throw Error('Get price error'); + prices = (await response.json()) as any[]; + } catch (err) { + console.error(err); + } + + let priceHash: PriceHash = {}; + // need to sort from symbol->denom input to make testnet denoms work (prices always come with mainnet denoms) + if (tokenList && tokenList.length > 0) { + prices.forEach((element) => { + if (element.symbol) { + const testnet_element = tokenList.find(({ symbol }) => symbol == element.symbol); + if (testnet_element) { + priceHash[testnet_element.base] = element.price; + } else { + priceHash[element.denom] = element.price; + } + } else { + priceHash[element.denom] = element.price; + } + }); + } else { + priceHash = prices.reduce( + (prev: any, cur: { denom: any; price: any }) => ({ + ...prev, + [cur.denom]: cur.price, + }), + {}, + ); + } + + return priceHash; +}; + +interface baseFee { + base_fee?: string; +} + +export const getEIP1559DynamicBaseFee = async (url: string) => { + let fee: baseFee | undefined = undefined; + try { + const response = await fetch(url); + if (!response.ok) throw Error('Get base fee error'); + fee = (await response.json()) as baseFee; + } catch (err) { + console.error(err); + console.error('Fetch base fee failed, retrying.'); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + try { + const response = await fetch(url); + if (response.ok) { + fee = (await response.json()) as baseFee; + } + } catch (err2) { + console.error('Osmosis: Get dynamic base fee error, returning default number.'); + } + } + if (fee && fee.base_fee) { + const finalFee = Number(fee!.base_fee!).toString(); + return finalFee; + } + return ''; +}; diff --git a/src/chains/cosmos/cosmos.requests.ts b/src/chains/cosmos/cosmos.requests.ts new file mode 100755 index 0000000000..3df4088fda --- /dev/null +++ b/src/chains/cosmos/cosmos.requests.ts @@ -0,0 +1,37 @@ +import { DecodedTxRaw } from '@cosmjs/proto-signing/build'; +export interface CosmosBalanceRequest { + address: string; // the user's Cosmos address as Bech32 + tokenSymbols: string[]; // a list of token symbol +} + +export interface CosmosBalanceResponse { + network: string; + timestamp: number; + latency: number; + balances: Record; +} + +export interface CosmosTokenRequest { + address: string; + token: string; +} + +export interface CosmosPollRequest { + txHash?: string; +} + +export enum TransactionResponseStatusCode { + FAILED = -1, + CONFIRMED = 1, +} + +export interface CosmosPollResponse { + network: string; + timestamp: number; + txHash: string; + currentBlock: number; + txBlock: number; + gasUsed: number; + gasWanted: number; + txData: DecodedTxRaw | null; +} diff --git a/src/chains/cosmos/cosmos.ts b/src/chains/cosmos/cosmos.ts new file mode 100755 index 0000000000..6b7724e2b4 --- /dev/null +++ b/src/chains/cosmos/cosmos.ts @@ -0,0 +1,164 @@ +import fse from 'fs-extra'; + +import { logger } from '../../services/logger'; +import { walletPath } from '../../wallet/utils'; + +import { CosmosBase } from './cosmos-base'; +import { getCosmosConfig } from './cosmos.config'; +import { isValidCosmosAddress } from './cosmos.validators'; + +const exampleCosmosPublicKey = 'cosmos000000000000000000000000000000000000000'; + +export class Cosmos extends CosmosBase { + private static _instances: { [name: string]: Cosmos }; + private static _walletAddressExample: string | null = null; + private _requestCount: number; + private _metricsLogInterval: number; + private _metricTimer; + public gasPrice: number; + public nativeTokenSymbol: string; + public chain: string; + public gasLimitTransaction: number = 200000; + + private constructor(network: string) { + const config = getCosmosConfig(network); + super( + network, + config.chainName, + config.network.nodeURL, + config.gasAdjustment, + config.feeTier, + config.manualGasPriceToken, + config.gasLimitTransaction, + config.allowedSlippage, + 'cosmos', + config.useEIP1559DynamicBaseFeeInsteadOfManualGasPrice, + config.rpcAddressDynamicBaseFee, + config.manualGasPrice, + ); + this.chain = network; + this.nativeTokenSymbol = config.nativeCurrencySymbol; + this.gasLimitTransaction = config.gasLimitTransaction; + this.gasPrice = config.manualGasPrice; + + this._requestCount = 0; + this._metricsLogInterval = 300000; // 5 minutes + this._metricTimer = setInterval(this.metricLogger.bind(this), this.metricsLogInterval); + } + + public static getInstance(network: string): Cosmos { + if (Cosmos._instances === undefined) { + Cosmos._instances = {}; + } + if (!(network in Cosmos._instances)) { + Cosmos._instances[network] = new Cosmos(network); + } + return Cosmos._instances[network]; + } + + public static getConnectedInstances(): { [name: string]: Cosmos } { + return Cosmos._instances; + } + + /** + * Validate Cosmos address format + * @param address The address to validate + * @returns The checksummed address if valid + * @throws Error if the address is invalid + */ + public static validateAddress(address: string): string { + try { + return isValidCosmosAddress(address); + } catch (error) { + throw new Error(`Invalid Cosmos address format: ${address}`); + } + } + + // Add new method to get first wallet address + public static async getFirstWalletAddress(): Promise { + const path = `${walletPath}/cosmos`; + try { + // Create directory if it doesn't exist + await fse.ensureDir(path); + + // Get all .json files in the directory + const files = await fse.readdir(path); + const walletFiles = files.filter((f) => f.endsWith('.json')); + + if (walletFiles.length === 0) { + return null; + } + + // Get the first wallet address (without .json extension) + const walletAddress = walletFiles[0].slice(0, -5); + + try { + // Attempt to validate the address + if (isValidCosmosAddress(walletAddress)) { + return walletAddress; + } + } catch (e) { + logger.warn(`Invalid Cosmos/Osmosis address found in wallet directory: ${walletAddress}`); + return null; + } + } catch (err) { + return null; + } + } + + /** + * Get a wallet address example for schema documentation + */ + public static async getWalletAddressExample(): Promise { + const defaultAddress = exampleCosmosPublicKey; + try { + const foundWallet = await Cosmos.getFirstWalletAddress(); + if (foundWallet) { + Cosmos._walletAddressExample = foundWallet; + return foundWallet; + } + logger.debug('No wallets found for examples in schema, using default.'); + Cosmos._walletAddressExample = defaultAddress; + return defaultAddress; + } catch (error) { + logger.error(`Error getting Cosmos/Osmosis wallet address for example: ${error.message}`); + return defaultAddress; + } + } + + public requestCounter(msg: any): void { + if (msg.action === 'request') this._requestCount += 1; + } + + public metricLogger(): void { + logger.info(this.requestCount + ' request(s) sent in last ' + this.metricsLogInterval / 1000 + ' seconds.'); + this._requestCount = 0; // reset + } + + // public get gasPrice(): number { + // return this._gasPrice; + // } + + // public get chain(): string { + // return this._chain; + // } + + // public get nativeTokenSymbol(): string { + // return this._nativeTokenSymbol; + // } + + public get requestCount(): number { + return this._requestCount; + } + + public get metricsLogInterval(): number { + return this._metricsLogInterval; + } + + async close() { + clearInterval(this._metricTimer); + if (this.chain in Cosmos._instances) { + delete Cosmos._instances[this.chain]; + } + } +} diff --git a/src/chains/cosmos/cosmos.universaltypes.ts b/src/chains/cosmos/cosmos.universaltypes.ts new file mode 100755 index 0000000000..6a05ef29dc --- /dev/null +++ b/src/chains/cosmos/cosmos.universaltypes.ts @@ -0,0 +1,302 @@ +import { Asset as CurrentAsset } from '@chain-registry/types'; // latest version , DenomUnit as AssetDenomUnit + +export const getExponentForAsset = (asset: CurrentAsset | FormerAsset | any): number => { + const denomUnits = (asset as any).denomUnits ?? (asset as any).denom_units; + if (asset && denomUnits) { + const unit = denomUnits.find(({ denom }: DenomUnit) => denom === asset.display); + if (unit) { + return unit.exponent; + } + } else if (asset.decimals) { + return asset.decimals; + } + return 0; +}; + +export interface AssetList { + $schema?: string; + chainName: string; + chain_name: string; + assets: CosmosAsset[]; +} + +export class CosmosAssetPrice { + poolId: string; + denom: string; +} + +// Newer universal type due to changes in Cosmos/Osmosis Asset formats and discrepancies between versions +export class CosmosAsset { + // instead of implements CurrentAsset, FormerAsset - everything recreated here (for constructor) + exponent: number = 0; + decimals: number = 0; + display: string; + chainName: string; + sourceDenom: string; + coinMinimalDenom: string; + price?: CosmosAssetPrice; + constructor(asset: CurrentAsset | FormerAsset) { + const _logoURIs = (asset as any).logoURIs ?? (asset as any).logo_URIs; + const _denomUnits = (asset as any).denomUnits ?? (asset as any).denom_units ?? []; + const _coingeckoId = (asset as any).coingeckoId ?? (asset as any).coingecko_id; + const _extendedDescription = (asset as any).extendedDescription ?? (asset as any).extended_description; + const _typeAsset = (asset as any).typeAsset ?? (asset as any).type_asset ?? ''; + const _decimals = (asset as any).decimals ?? getExponentForAsset(asset); + const _price = (asset as any).price ?? undefined; + const _base = (asset as any).base ?? (asset as any).sourceDenom; + const _address = (asset as any).address ?? (asset as any).sourceDenom; + + this.base = this.sourceDenom = this.denom = this.display = _base; + this.coinMinimalDenom = (asset as any).coinMinimalDenom ?? _base; // can be ibc/ADDRESS string or sourceDenom + this.logoURIs = this.logo_URIs = _logoURIs; + if (_denomUnits && _denomUnits.length > 0) { + this.denomUnits = this.denom_units = _denomUnits; + } else { + this.denomUnits = this.denom_units = [ + { denom: _base, exponent: _decimals }, + { denom: asset.symbol, exponent: 0 }, + ]; + } + this.coingeckoId = this.coingecko_id = _coingeckoId; + this.extendedDescription = this.extended_description = _extendedDescription; + this.typeAsset = this.type_asset = _typeAsset; + this.decimals = this.exponent = _decimals; + this.price = _price; + this.description = asset.description; + this.address = _address; + this.name = asset.name; + this.symbol = asset.symbol; + this.keywords = asset.keywords; + this.chainName = 'osmosis'; // don't use osmosistestnet here as it messes with some import calcs + + if (asset.ibc) { + const _sourceChannel = (asset as any).sourceChannel ?? (asset as any).source_channel; + const _sourceDenom = (asset as any).sourceDenom ?? (asset as any).source_denom; + const _dstChannel = (asset as any).dstChannel ?? (asset as any).dst_channel; + + this.ibc = { + sourceChannel: _sourceChannel, + sourceDenom: _sourceDenom, + dstChannel: _dstChannel, + source_channel: _sourceChannel, + dst_channel: _dstChannel, + source_denom: _sourceDenom, + }; + } + } + deprecated?: boolean; + extendedDescription?: string; + extended_description?: string; + denom_units: DenomUnit[]; + type_asset: + | 'sdk.coin' + | 'cw20' + | 'erc20' + | 'ics20' + | 'snip20' + | 'snip25' + | 'bitcoin-like' + | 'evm-base' + | 'svm-base' + | 'substrate' + | 'sdk.factory' + | 'bitsong' + | 'unknown'; + typeAsset: + | 'sdk.coin' + | 'cw20' + | 'erc20' + | 'ics20' + | 'snip20' + | 'snip25' + | 'bitcoin-like' + | 'evm-base' + | 'svm-base' + | 'substrate' + | 'sdk.factory' + | 'bitsong' + | 'unknown'; + traces?: (IbcTransition | IbcCw20Transition | IbcBridgeTransition | NonIbcTransition)[]; + logo_URIs?: { png?: string; svg?: string }; + images?: { + image_sync?: Pointer; + png?: string; + svg?: string; + theme?: { + primaryColorHex?: string; + primary_color_hex?: string; + backgroundColorHex?: string; + background_color_hex?: string; + circle?: boolean; + darkMode?: boolean; + dark_mode?: boolean; + monochrome?: boolean; + }; + }[]; + coingecko_id?: string; + socials?: { + website?: string; + twitter?: string; + telegram?: string; + discord?: string; + github?: string; + medium?: string; + reddit?: string; + }; + description?: string; + address: string = ''; + denomUnits: DenomUnit[] = []; + base: string; // this is denom!!! + denom: string; // this is denom!!! + name: string; + symbol: string; + logoURIs?: { + png?: string; + svg?: string; + jpeg?: string; + }; + coingeckoId?: string; + keywords?: string[]; + ibc?: { + sourceChannel: string; + sourceDenom: string; + dstChannel: string; + source_channel: string; + dst_channel: string; + source_denom: string; + }; +} + +export interface DenomUnit { + denom: string; + exponent: number; + aliases?: string[]; +} +export interface Pointer { + chainName: string; + chain_name: string; + baseDenom?: string; + base_denom?: string; +} +export interface IbcTransition { + type: 'ibc'; + counterparty: { + chainName: string; + baseDenom: string; + channelId: string; + chain_name: string; + base_denom: string; + channel_id: string; + }; + chain: { + channelId: string; + channel_id: string; + path?: string; + }; +} +export interface IbcCw20Transition { + type: 'ibc-cw20'; + counterparty: { + chainName: string; + baseDenom: string; + port: string; + channelId: string; + chain_name: string; + base_denom: string; + channel_id: string; + }; + chain: { + port: string; + channelId: string; + channel_id: string; + path?: string; + }; +} +export interface IbcBridgeTransition { + type: 'ibc-bridge'; + counterparty: { + chain_name: string; + base_denom: string; + port?: string; + channel_id: string; + }; + chain: { + port?: string; + channel_id: string; + path?: string; + }; + provider: string; +} +export interface NonIbcTransition { + type: 'bridge' | 'liquid-stake' | 'synthetic' | 'wrapped' | 'additional-mintage' | 'test-mintage' | 'legacy-mintage'; + counterparty: { + chainName: string; + baseDenom: string; + chain_name: string; + base_denom: string; + contract?: string; + }; + chain?: { + contract: string; + }; + provider: string; +} + +// Pasting old type from @chain-registry/types +export interface FormerAsset { + deprecated?: boolean; + description?: string; + chainName: string; + extended_description?: string; + denom_units: DenomUnit[]; + type_asset: + | 'sdk.coin' + | 'cw20' + | 'erc20' + | 'ics20' + | 'snip20' + | 'snip25' + | 'bitcoin-like' + | 'evm-base' + | 'svm-base' + | 'substrate' + | 'unknown'; + address?: string; + base: string; + name: string; + display: string; + symbol: string; + traces?: (IbcTransition | IbcCw20Transition | IbcBridgeTransition | NonIbcTransition)[]; + ibc?: { + source_channel: string; + dst_channel: string; + source_denom: string; + }; + logo_URIs?: { + png?: string; + svg?: string; + }; + images?: { + image_sync?: Pointer; + png?: string; + svg?: string; + theme?: { + primary_color_hex?: string; + background_color_hex?: string; + circle?: boolean; + dark_mode?: boolean; + monochrome?: boolean; + }; + }[]; + coingecko_id?: string; + keywords?: string[]; + socials?: { + website?: string; + twitter?: string; + telegram?: string; + discord?: string; + github?: string; + medium?: string; + reddit?: string; + }; +} diff --git a/src/chains/cosmos/cosmos.validators.ts b/src/chains/cosmos/cosmos.validators.ts new file mode 100755 index 0000000000..2d382727dd --- /dev/null +++ b/src/chains/cosmos/cosmos.validators.ts @@ -0,0 +1,18 @@ +import { normalizeBech32, fromHex } from '@cosmjs/encoding'; + +export const isValidCosmosAddress = (str: string): string => { + try { + normalizeBech32(str); + return str; + } catch (e) { + return undefined; + } +}; +export const isValidCosmosPrivateKey = (str: string): boolean => { + try { + fromHex(str); + return true; + } catch (e) { + return false; + } +}; diff --git a/src/chains/solana/solana-priority-fees.ts b/src/chains/solana/solana-priority-fees.ts index bf457e4f88..50974689ba 100644 --- a/src/chains/solana/solana-priority-fees.ts +++ b/src/chains/solana/solana-priority-fees.ts @@ -27,7 +27,7 @@ export class SolanaPriorityFees { try { // Try to get Helius API key from RPC config - const { ConfigManagerV2 } = await import('../../services/config-manager-v2'); + const { ConfigManagerV2 } = await import('../../services/config-manager-v2.js'); const configManager = ConfigManagerV2.getInstance(); const apiKey = configManager.get('helius.apiKey') || ''; diff --git a/src/chains/solana/solana.ts b/src/chains/solana/solana.ts index f5b5769f2c..8b2b22e4a1 100644 --- a/src/chains/solana/solana.ts +++ b/src/chains/solana/solana.ts @@ -618,7 +618,7 @@ export class Solana { // Try Meteora try { const { getPositionsOwned: getMeteoraPositions } = await import( - '../../connectors/meteora/clmm-routes/positionsOwned' + '../../connectors/meteora/clmm-routes/positionsOwned.js' ); // Create a minimal fastify-like object for validation const mockFastify = { httpErrors: { badRequest: (msg: string) => new Error(msg) } }; @@ -644,7 +644,7 @@ export class Solana { // Try Raydium CLMM try { const { getPositionsOwned: getRaydiumPositions } = await import( - '../../connectors/raydium/clmm-routes/positionsOwned' + '../../connectors/raydium/clmm-routes/positionsOwned.js' ); const mockFastify = { httpErrors: { badRequest: (msg: string) => new Error(msg) } }; const raydiumPositions = await getRaydiumPositions(mockFastify as any, this.network, walletAddress); @@ -669,7 +669,7 @@ export class Solana { // Try PancakeSwap try { const { getPositionsOwned: getPancakeswapPositions } = await import( - '../../connectors/pancakeswap-sol/clmm-routes/positionsOwned' + '../../connectors/pancakeswap-sol/clmm-routes/positionsOwned.js' ); const mockFastify = { httpErrors: { badRequest: (msg: string) => new Error(msg) } }; const pancakeswapPositions = await getPancakeswapPositions(mockFastify as any, this.network, walletAddress); diff --git a/src/config/routes/getChains.ts b/src/config/routes/getChains.ts index 1f22242f4f..2e2de56a0f 100644 --- a/src/config/routes/getChains.ts +++ b/src/config/routes/getChains.ts @@ -49,7 +49,7 @@ export const getChainsRoute: FastifyPluginAsync = async (fastify) => { const network = networkParts.join('-'); // Handle networks like mainnet-beta // Only process known chains - if (['ethereum', 'solana'].includes(chain)) { + if (['ethereum', 'solana', 'cosmos'].includes(chain)) { if (!chainNetworks[chain]) { chainNetworks[chain] = []; } @@ -64,6 +64,9 @@ export const getChainsRoute: FastifyPluginAsync = async (fastify) => { if (!chainNetworks['solana']) { chainNetworks['solana'] = []; } + if (!chainNetworks['cosmos']) { + chainNetworks['cosmos'] = []; + } const chains = Object.entries(chainNetworks).map(([chain, networks]) => ({ chain, diff --git a/src/config/routes/getConnectors.ts b/src/config/routes/getConnectors.ts index 76a329bbbc..7672864d23 100644 --- a/src/config/routes/getConnectors.ts +++ b/src/config/routes/getConnectors.ts @@ -6,6 +6,7 @@ import { PancakeswapConfig } from '#src/connectors/pancakeswap/pancakeswap.confi import { ZeroXConfig } from '../../connectors/0x/0x.config'; import { JupiterConfig } from '../../connectors/jupiter/jupiter.config'; import { MeteoraConfig } from '../../connectors/meteora/meteora.config'; +import { OsmosisConfig } from '../../connectors/osmosis/osmosis.config'; import { PancakeswapSolConfig } from '../../connectors/pancakeswap-sol/pancakeswap-sol.config'; import { RaydiumConfig } from '../../connectors/raydium/raydium.config'; import { UniswapConfig } from '../../connectors/uniswap/uniswap.config'; @@ -70,6 +71,12 @@ export const connectorsConfig = [ chain: PancakeswapSolConfig.chain, networks: [...PancakeswapSolConfig.networks], }, + { + name: 'osmosis', + trading_types: [...OsmosisConfig.tradingTypes], + chain: OsmosisConfig.chain, + networks: [...OsmosisConfig.networks], + }, ]; export const getConnectorsRoute: FastifyPluginAsync = async (fastify) => { diff --git a/src/config/utils.ts b/src/config/utils.ts index a76c150442..90853f67dd 100644 --- a/src/config/utils.ts +++ b/src/config/utils.ts @@ -1,19 +1,18 @@ -import * as fs from 'fs'; -import * as path from 'path'; - import { FastifyInstance } from 'fastify'; -import * as yaml from 'js-yaml'; +import createError from 'http-errors'; +// createError used as a workaround to enable (actual, project-wide, sans jest) debugging, +// since fastify.httpErrors plugin is not being loaded prior to ts-compile. import { ConfigManagerV2 } from '../services/config-manager-v2'; import { logger } from '../services/logger'; -export const getConfig = (fastify: FastifyInstance, namespace?: string): object => { +export const getConfig = (_fastify: FastifyInstance, namespace?: string): object => { if (namespace) { logger.info(`Getting configuration for namespace: ${namespace}`); const namespaceConfig = ConfigManagerV2.getInstance().getNamespace(namespace); if (!namespaceConfig) { - throw fastify.httpErrors.notFound(`Namespace '${namespace}' not found`); + throw createError(404, `Namespace '${namespace}' not found`); } return namespaceConfig.configuration; @@ -23,32 +22,31 @@ export const getConfig = (fastify: FastifyInstance, namespace?: string): object return ConfigManagerV2.getInstance().allConfigurations; }; -export const updateConfig = (fastify: FastifyInstance, configPath: string, configValue: any): void => { +export const updateConfig = (_fastify: FastifyInstance, configPath: string, configValue: any): void => { logger.info(`Updating config path: ${configPath} with value: ${JSON.stringify(configValue)}`); - try { // Update the configuration using ConfigManagerV2 ConfigManagerV2.getInstance().set(configPath, configValue); logger.info(`Successfully updated configuration: ${configPath}`); } catch (error) { logger.error(`Failed to update configuration: ${error.message}`); - throw fastify.httpErrors.internalServerError(`Failed to update configuration: ${error.message}`); + throw createError(500, `Failed to update configuration: ${error.message}`); } }; export const getDefaultPools = async ( - fastify: FastifyInstance, + _fastify: FastifyInstance, connector: string, network: string, ): Promise> => { // Import PoolService here to avoid circular dependency - const { PoolService } = await import('../services/pool-service'); + const { PoolService } = await import('../services/pool-service.js'); // Parse connector name to extract base connector and type const [baseConnector, poolType] = connector.split('/'); if (!baseConnector) { - throw fastify.httpErrors.badRequest('Connector name is required'); + throw createError(400, 'Connector name is required'); } // Determine the pool type (amm or clmm) @@ -73,8 +71,7 @@ export const getDefaultPools = async ( // Note: Pool management functions have been moved to PoolService // Use the /pools endpoints for pool management - -export const updateDefaultWallet = (fastify: FastifyInstance, chain: string, walletAddress: string): void => { +export const updateDefaultWallet = (_fastify: FastifyInstance, chain: string, walletAddress: string): void => { logger.info(`Updating default wallet for ${chain} to: ${walletAddress}`); try { @@ -84,6 +81,6 @@ export const updateDefaultWallet = (fastify: FastifyInstance, chain: string, wal logger.info(`Successfully updated default wallet for ${chain}`); } catch (error) { logger.error(`Failed to update default wallet: ${error.message}`); - throw fastify.httpErrors.internalServerError(`Failed to update default wallet: ${error.message}`); + throw createError(500, `Failed to update default wallet: ${error.message}`); } }; diff --git a/src/connectors/meteora/clmm-routes/executeSwap.ts b/src/connectors/meteora/clmm-routes/executeSwap.ts index 71f624f887..ff134c9cd1 100644 --- a/src/connectors/meteora/clmm-routes/executeSwap.ts +++ b/src/connectors/meteora/clmm-routes/executeSwap.ts @@ -171,7 +171,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { } // Use PoolService to find pool by token pair - const { PoolService } = await import('../../../services/pool-service'); + const { PoolService } = await import('../../../services/pool-service.js'); const poolService = PoolService.getInstance(); const pool = await poolService.getPool( diff --git a/src/connectors/meteora/clmm-routes/quoteSwap.ts b/src/connectors/meteora/clmm-routes/quoteSwap.ts index 2e66a4a23c..e8a3411156 100644 --- a/src/connectors/meteora/clmm-routes/quoteSwap.ts +++ b/src/connectors/meteora/clmm-routes/quoteSwap.ts @@ -190,7 +190,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { } // Use PoolService to find pool by token pair - const { PoolService } = await import('../../../services/pool-service'); + const { PoolService } = await import('../../../services/pool-service.js'); const poolService = PoolService.getInstance(); const pool = await poolService.getPool( diff --git a/src/connectors/osmosis/amm-routes/addLiquidity.ts b/src/connectors/osmosis/amm-routes/addLiquidity.ts new file mode 100755 index 0000000000..549939c854 --- /dev/null +++ b/src/connectors/osmosis/amm-routes/addLiquidity.ts @@ -0,0 +1,118 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { + AddLiquidityRequestType as AMMAddLiquidityRequestType, + AddLiquidityResponseType as AMMAddLiquidityResponseType, + AddLiquidityRequest as AMMAddLiquidityRequest, + AddLiquidityResponse as AMMAddLiquidityResponse, +} from '../../../schemas/amm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function addLiquidity( + fastify: any, + req: AMMAddLiquidityRequestType, +): Promise { + let networkToUse = req.network ? req.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + + const response: AMMAddLiquidityResponseType = await osmosis.controller.addLiquidityAMM(osmosis, fastify, req); + return response; +} + +export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { + await fastify.register(require('@fastify/sensible')); + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + fastify.post<{ + Body: AMMAddLiquidityRequestType; + Reply: AMMAddLiquidityResponseType; + }>( + '/add-liquidity', + { + schema: { + description: 'Add liquidity to a Osmosis GAMM pool', + tags: ['osmosis/connector/amm'], + body: { + ...AMMAddLiquidityRequest, + properties: { + ...AMMAddLiquidityRequest.properties, + network: { type: 'string', default: 'base' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + poolAddress: { + type: 'string', + examples: [''], + }, + baseToken: { type: 'string', examples: ['OSMO'] }, + quoteToken: { type: 'string', examples: ['ION'] }, + baseTokenAmount: { type: 'number', examples: [0.001] }, + quoteTokenAmount: { type: 'number', examples: [2.5] }, + slippagePct: { type: 'number', examples: [1] }, + }, + }, + response: { + 200: AMMAddLiquidityResponse, + }, + }, + }, + async (request) => { + try { + const { + network, + poolAddress, + baseTokenAmount, + quoteTokenAmount, + slippagePct, + walletAddress: requestedWalletAddress, + } = request.body; + + if ( + !baseTokenAmount || + !quoteTokenAmount || + !slippagePct || + !requestedWalletAddress || + !network || + !poolAddress + ) { + throw fastify.httpErrors.badRequest('Missing required parameters'); + } + + // Get wallet address - either from request or first available + let walletAddress = requestedWalletAddress; + if (!walletAddress) { + walletAddress = await Osmosis.getFirstWalletAddress(); + if (!walletAddress) { + throw fastify.httpErrors.badRequest('No wallet address provided and no wallets found.'); + } + logger.info(`Using first available wallet address: ${walletAddress}`); + } + + return await addLiquidity(fastify, request.body); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + + // Handle specific user-actionable errors + if (e.message && e.message.includes('Insufficient allowance')) { + throw fastify.httpErrors.badRequest(e.message); + } + + // Handle insufficient funds errors + if (e.code === 'INSUFFICIENT_FUNDS' || (e.message && e.message.includes('insufficient funds'))) { + throw fastify.httpErrors.badRequest( + 'Insufficient ETH balance to pay for gas fees. Please add more ETH to your wallet.', + ); + } + + throw fastify.httpErrors.internalServerError('Failed to add liquidity'); + } + }, + ); +}; + +export default addLiquidityRoute; diff --git a/src/connectors/osmosis/amm-routes/executeSwap.ts b/src/connectors/osmosis/amm-routes/executeSwap.ts new file mode 100755 index 0000000000..ef757b5728 --- /dev/null +++ b/src/connectors/osmosis/amm-routes/executeSwap.ts @@ -0,0 +1,76 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { ExecuteSwapRequestType, ExecuteSwapResponseType, ExecuteSwapResponse } from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; +import { executeSwap } from '../osmosis.swap'; + +export const executeSwapRoute: FastifyPluginAsync = async (fastify, _options) => { + // Import the httpErrors plugin to ensure it's available + await fastify.register(require('@fastify/sensible')); + const walletAddressExample = await Osmosis.getWalletAddressExample(); + const { ConfigManagerV2 } = require('../../../services/config-manager-v2'); + const osmosisNetworks = ['testnet', 'mainnet']; + + fastify.post<{ + Body: ExecuteSwapRequestType; + Reply: ExecuteSwapResponseType; + }>( + '/execute-swap', + { + schema: { + description: 'Execute a swap using Osmosis Order Router', + tags: ['osmosis'], + body: { + type: 'object', + properties: { + network: { + type: 'string', + default: 'mainnet', + enum: osmosisNetworks, + }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + baseToken: { type: 'string', examples: ['WETH'] }, + quoteToken: { type: 'string', examples: ['USDC'] }, + amount: { type: 'number', examples: [0.001] }, + side: { type: 'string', enum: ['BUY', 'SELL'], examples: ['SELL'] }, + slippagePct: { type: 'number', examples: [0.5] }, + }, + required: ['walletAddress', 'baseToken', 'quoteToken', 'amount', 'side'], + }, + response: { + 200: ExecuteSwapResponse, + }, + }, + }, + async (request, reply) => { + try { + // Log the request parameters for debugging + logger.info(`Received execute-swap request: ${JSON.stringify(request.body)}`); + const { baseToken: baseTokenSymbol, quoteToken: quoteTokenSymbol, amount, side, poolAddress } = request.body; + + // Validate essential parameters + if (!poolAddress || !baseTokenSymbol || !quoteTokenSymbol || !amount || !side) { + logger.error('Missing required parameters in request'); + return reply.badRequest('Missing required parameters'); + } + + const executeSwapResponse = await executeSwap(fastify, request.body, 'amm'); + return executeSwapResponse; + } catch (e) { + logger.error(`Execute swap error: ${e.message}`); + if (e.stack) { + logger.debug(`Error stack: ${e.stack}`); + } + + if (e.code === 'UNPREDICTABLE_GAS_LIMIT') { + return reply.badRequest('Transaction failed: Insufficient funds or gas estimation error'); + } + + return reply.internalServerError(`Failed to execute swap: ${e.message}`); + } + }, + ); +}; + +export default executeSwapRoute; diff --git a/src/connectors/osmosis/amm-routes/fetchPools.ts b/src/connectors/osmosis/amm-routes/fetchPools.ts new file mode 100644 index 0000000000..2f80a016ac --- /dev/null +++ b/src/connectors/osmosis/amm-routes/fetchPools.ts @@ -0,0 +1,69 @@ +import { Type } from '@sinclair/typebox'; +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { PoolInfoSchema, FetchPoolsRequestType } from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; +import { FetchPoolsRequest, SerializableExtendedPool } from '../osmosis.types'; + +export async function fetchPools( + fastify: FastifyInstance, + request: FetchPoolsRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = await osmosis.controller.fetchPoolsForTokens(osmosis, fastify, request, poolType); + return response; +} + +export const fetchPoolsRoute: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: FetchPoolsRequestType; + Reply: Record; + }>( + '/fetch-pools', + { + schema: { + description: 'Fetch info about Osmosis pools', + tags: ['/connector/osmosis/amm'], + querystring: FetchPoolsRequest, + response: { + 200: Type.Array(PoolInfoSchema), + }, + }, + }, + async (request): Promise => { + try { + const { tokenA, tokenB } = request.query; + + if (!tokenA || !tokenB) { + throw fastify.httpErrors.badRequest('Both baseToken and quoteToken must be provided'); + } + + return await fetchPools(fastify, request.body, 'amm'); + } catch (e) { + logger.error(`Error in pool-info route: ${e.message}`); + if (e.stack) { + logger.debug(`Stack trace: ${e.stack}`); + } + + // Return appropriate error based on the error message + if (e.statusCode) { + throw e; // Already a formatted Fastify error + } else if (e.message && e.message.includes('invalid address')) { + throw fastify.httpErrors.badRequest(`Invalid pool address`); + } else if (e.message && e.message.includes('not found')) { + throw fastify.httpErrors.notFound(e.message); + } else { + throw fastify.httpErrors.internalServerError(`Failed to fetch pool info: ${e.message}`); + } + } + }, + ); +}; + +export default fetchPoolsRoute; diff --git a/src/connectors/osmosis/amm-routes/index.ts b/src/connectors/osmosis/amm-routes/index.ts new file mode 100644 index 0000000000..a7fc0989c0 --- /dev/null +++ b/src/connectors/osmosis/amm-routes/index.ts @@ -0,0 +1,21 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { addLiquidityRoute } from './addLiquidity'; +import { executeSwapRoute } from './executeSwap'; +import { poolInfoRoute } from './poolInfo'; +import { positionInfoRoute } from './positionInfo'; +import { positionsOwnedRoute } from './positionsOwned'; +import { quoteSwapRoute } from './quoteSwap'; +import { removeLiquidityRoute } from './removeLiquidity'; + +export const osmosisAmmRoutes: FastifyPluginAsync = async (fastify) => { + await fastify.register(poolInfoRoute); + await fastify.register(positionInfoRoute); + await fastify.register(quoteSwapRoute); + await fastify.register(positionsOwnedRoute); + await fastify.register(executeSwapRoute); + await fastify.register(addLiquidityRoute); + await fastify.register(removeLiquidityRoute); +}; + +export default osmosisAmmRoutes; diff --git a/src/connectors/osmosis/amm-routes/poolInfo.ts b/src/connectors/osmosis/amm-routes/poolInfo.ts new file mode 100644 index 0000000000..6bfd730ae4 --- /dev/null +++ b/src/connectors/osmosis/amm-routes/poolInfo.ts @@ -0,0 +1,91 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + GetPoolInfoRequest, + PoolInfoSchema, + PoolInfo as AMMPoolInfo, + GetPoolInfoRequestType as AMMGetPoolInfoRequestType, +} from '../../../schemas/amm-schema'; +import { + PoolInfo as CLMMPoolInfo, + GetPoolInfoRequestType as CLMMGetPoolInfoRequestType, +} from '../../../schemas/clmm-schema'; +import { ConfigManagerV2 } from '../../../services/config-manager-v2'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; +const osmosisNetworks = ['testnet', 'mainnet']; + +export async function poolInfo( + fastify: FastifyInstance, + request: AMMGetPoolInfoRequestType | CLMMGetPoolInfoRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = (await osmosis.controller.poolInfoRequest(osmosis, fastify, request, poolType)) as AMMPoolInfo; + return response; +} + +export const poolInfoRoute: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: AMMGetPoolInfoRequestType; + Reply: Record; + }>( + '/pool-info', + { + schema: { + description: 'Get AMM pool information from Osmosis', + tags: ['osmosis/connector/amm'], + querystring: { + ...GetPoolInfoRequest, + properties: { + network: { + type: 'string', + default: 'mainnet', + enum: ['testnet', 'mainnet'], //osmosisNetworks + }, + poolAddress: { + type: 'string', + examples: ['osmo1500hy75krs9e8t50aav6fahk8sxhajn9ctp40qwvvn8tcprkk6wszun4a5'], + }, + }, + }, + response: { + 200: PoolInfoSchema, + }, + }, + }, + async (request): Promise => { + try { + const { poolAddress } = request.query; + + if (!poolAddress) { + throw fastify.httpErrors.badRequest('Pool address must be provided'); + } + + return (await poolInfo(fastify, request.query, 'amm')) as AMMPoolInfo; + } catch (e) { + logger.error(`Error in pool-info route: ${e.message}`); + if (e.stack) { + logger.debug(`Stack trace: ${e.stack}`); + } + + // Return appropriate error based on the error message + if (e.statusCode) { + throw e; // Already a formatted Fastify error + } else if (e.message && e.message.includes('invalid address')) { + throw fastify.httpErrors.badRequest(`Invalid pool address`); + } else if (e.message && e.message.includes('not found')) { + throw fastify.httpErrors.notFound(e.message); + } else { + throw fastify.httpErrors.internalServerError(`Failed to fetch pool info: ${e.message}`); + } + } + }, + ); +}; + +export default poolInfoRoute; diff --git a/src/connectors/osmosis/amm-routes/positionInfo.ts b/src/connectors/osmosis/amm-routes/positionInfo.ts new file mode 100644 index 0000000000..8ebc96816f --- /dev/null +++ b/src/connectors/osmosis/amm-routes/positionInfo.ts @@ -0,0 +1,79 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + PositionInfo as AMMPositionInfo, + PositionInfoSchema as AMMPositionInfoSchema, + GetPositionInfoRequestType as AMMGetPositionInfoRequestType, + GetPositionInfoRequest as AMMGetPositionInfoRequest, +} from '../../../schemas/amm-schema'; +import { + PositionInfo as CLMMPositionInfo, + GetPositionInfoRequestType as CLMMGetPositionInfoRequestType, +} from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function positionInfo( + fastify: FastifyInstance, + request: AMMGetPositionInfoRequestType | CLMMGetPositionInfoRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = await osmosis.controller.poolPosition(osmosis, fastify, request, poolType); + return response; +} + +export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + fastify.get<{ + Querystring: AMMGetPositionInfoRequestType; + Reply: AMMPositionInfo; + }>( + '/position-info', + { + schema: { + description: 'Get position information for a osmosis AMM pool', + tags: ['osmosis/connector/amm'], + querystring: { + ...AMMGetPositionInfoRequest, + properties: { + network: { type: 'string', default: 'base' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + poolAddress: { + type: 'string', + examples: [''], + }, + }, + }, + response: { + 200: AMMPositionInfoSchema, + }, + }, + }, + async (request) => { + try { + const { poolAddress: requestedPoolAddress } = request.query; + + // Validate essential parameters + if (!requestedPoolAddress) { + throw fastify.httpErrors.badRequest('Pool address must be provided'); + } + + return (await positionInfo(fastify, request.query, 'amm')) as AMMPositionInfo; + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw fastify.httpErrors.internalServerError('Failed to get position info'); + } + }, + ); +}; + +export default positionInfoRoute; diff --git a/src/connectors/osmosis/amm-routes/positionsOwned.ts b/src/connectors/osmosis/amm-routes/positionsOwned.ts new file mode 100644 index 0000000000..e76dc272c8 --- /dev/null +++ b/src/connectors/osmosis/amm-routes/positionsOwned.ts @@ -0,0 +1,92 @@ +import { Type, Static } from '@sinclair/typebox'; +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + PositionInfo as AMMPositionInfo, + PositionInfoSchema as AMMPositionInfoSchema, +} from '../../../schemas/amm-schema'; +import { + PositionInfoSchema as CLMMPositionInfoSchema, + GetPositionInfoRequest as CLMMGetPositionInfoRequest, +} from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +const PositionsOwnedRequest = Type.Object({ + network: Type.Optional(Type.String({ examples: ['mainnet'], default: 'mainnet' })), + walletAddress: Type.String({ examples: [''] }), +}); +type PositionsOwnedRequestType = Static; + +const AMMAllPositionsOwnedResponse = Type.Array(AMMPositionInfoSchema); +const CLMMAllPositionsOwnedResponse = Type.Array(CLMMPositionInfoSchema); +type AMMAllPositionsOwnedResponseType = Static; +type CLMMAllPositionsOwnedResponseType = Static; + +export async function positionsOwned( + fastify: FastifyInstance, + request: PositionsOwnedRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = (await osmosis.controller.allPoolPositions( + osmosis, + fastify, + request.walletAddress, + poolType, + )) as AMMAllPositionsOwnedResponseType; + return response; +} + +export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + fastify.get<{ + Querystring: typeof PositionsOwnedRequest.static; + Reply: AMMAllPositionsOwnedResponseType; + }>( + '/positions-owned', + { + schema: { + description: 'Get all AMM positions for wallet address', + tags: ['osmosis/connector'], + querystring: { + ...CLMMGetPositionInfoRequest, + properties: { + network: { type: 'string', default: 'mainnet' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + }, + }, + response: { + 200: CLMMPositionInfoSchema, + }, + }, + }, + async (request) => { + try { + const { walletAddress: requestedWalletAddress } = request.query; + + // Validate essential parameters + if (!requestedWalletAddress) { + throw fastify.httpErrors.badRequest( + 'Either pool address or both base token and quote token must be provided', + ); + } + + return (await positionsOwned(fastify, request.query, 'amm')) as unknown as AMMPositionInfo[]; + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw fastify.httpErrors.internalServerError('Failed to get position info'); + } + }, + ); +}; + +export default positionsOwnedRoute; diff --git a/src/connectors/osmosis/amm-routes/quoteSwap.ts b/src/connectors/osmosis/amm-routes/quoteSwap.ts new file mode 100755 index 0000000000..6f207fcae4 --- /dev/null +++ b/src/connectors/osmosis/amm-routes/quoteSwap.ts @@ -0,0 +1,122 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { QuoteSwapResponseType, QuoteSwapResponse, QuoteSwapRequestType } from '../../../schemas/clmm-schema'; +import { ConfigManagerV2 } from '../../../services/config-manager-v2'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; +import { quoteSwap } from '../osmosis.swap'; + +export const quoteSwapRoute: FastifyPluginAsync = async (fastify, _options) => { + // Import the httpErrors plugin to ensure it's available + await fastify.register(require('@fastify/sensible')); + + // Get first wallet address for example + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + // Get available networks from osmosis configuration (same method as chain.routes.ts) + const osmosisNetworks = ['testnet', 'mainnet']; + + fastify.get<{ + Querystring: QuoteSwapRequestType; + Reply: QuoteSwapResponseType; + }>( + '/quote-swap', + { + schema: { + description: 'Get a swap quote using Osmosis router', + tags: ['/connectors/osmosis/amm'], + querystring: { + type: 'object', + properties: { + network: { + type: 'string', + default: 'mainnet', + enum: osmosisNetworks, + }, + baseToken: { type: 'string', examples: ['ION'] }, + quoteToken: { type: 'string', examples: ['OSMO'] }, + 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'], + }, + response: { + 200: QuoteSwapResponse, + }, + }, + }, + async (request, reply) => { + try { + // Log the request parameters for debugging + logger.info(`Received quote-swap request: ${JSON.stringify(request.query)}`); + + const { + network, + baseToken: baseTokenSymbol, + quoteToken: quoteTokenSymbol, + amount, + side, + slippagePct, + } = request.query; + + // Validate essential parameters + if (!baseTokenSymbol || !quoteTokenSymbol || !amount || !side || !network || !slippagePct) { + logger.error('Missing required parameters in request'); + return reply.badRequest('Missing required parameters'); + } + + try { + // Use our shared quote function + const quoteResult = await quoteSwap(fastify, request.query, 'amm'); + + // Return only the data needed for the API response + return { + slippagePct: quoteResult.slippagePct, + poolAddress: quoteResult.poolAddress, + tokenIn: request.query.baseToken, + tokenOut: request.query.quoteToken, + amountIn: quoteResult.amountIn, + amountOut: quoteResult.amountOut, + price: quoteResult.price, + minAmountOut: quoteResult.minAmountOut, + maxAmountIn: quoteResult.maxAmountIn, + priceImpactPct: quoteResult.priceImpactPct, + }; + } catch (error) { + // If the error already has a status code, it's a Fastify HTTP error + if (error.statusCode) { + throw error; + } + + // Log more detailed information about the error + logger.error(`Router error: ${error.message}`); + if (error.stack) { + logger.debug(`Error stack: ${error.stack}`); + } + + // Check if there's any additional error details + if (error.innerError) { + logger.error(`Inner error: ${JSON.stringify(error.innerError)}`); + } + + // Check if it's a specific error type from the Alpha Router + if (error.name === 'SwapRouterError') { + logger.error(`SwapRouterError details: ${JSON.stringify(error)}`); + } + + return reply.badRequest(`Failed to get quote with router: ${error.message}`); + } + } catch (e) { + logger.error(`Quote swap error: ${e.message}`); + if (e.stack) { + logger.debug(`Error stack: ${e.stack}`); + } + return reply.internalServerError(`Failed to get quote: ${e.message}`); + } + }, + ); +}; + +export default quoteSwapRoute; diff --git a/src/connectors/osmosis/amm-routes/removeLiquidity.ts b/src/connectors/osmosis/amm-routes/removeLiquidity.ts new file mode 100755 index 0000000000..b33d418329 --- /dev/null +++ b/src/connectors/osmosis/amm-routes/removeLiquidity.ts @@ -0,0 +1,89 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { + RemoveLiquidityRequestType as AMMRemoveLiquidityRequestType, + RemoveLiquidityResponseType as AMMRemoveLiquidityResponseType, + RemoveLiquidityRequest as AMMRemoveLiquidityRequest, + RemoveLiquidityResponse as AMMRemoveLiquidityResponse, +} from '../../../schemas/amm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function removeLiquidity( + fastify: any, + req: AMMRemoveLiquidityRequestType, +): Promise { + let networkToUse = req.network ? req.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + + const response: AMMRemoveLiquidityResponseType = await osmosis.controller.removeLiquidityAMM(osmosis, fastify, req); + return response; +} + +export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { + await fastify.register(require('@fastify/sensible')); + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + fastify.post<{ + Body: AMMRemoveLiquidityRequestType; + Reply: AMMRemoveLiquidityResponseType; + }>( + '/remove-liquidity', + { + schema: { + description: 'Remove liquidity from an Osmosis GAMM pool', + tags: ['osmosis/connector/amm'], + body: { + ...AMMRemoveLiquidityRequest, + properties: { + ...AMMRemoveLiquidityRequest.properties, + network: { type: 'string', default: 'base' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + poolAddress: { + type: 'string', + examples: [''], + }, + percentageToRemove: { type: 'number', examples: [100] }, + }, + }, + response: { + 200: AMMRemoveLiquidityResponse, + }, + }, + }, + async (request) => { + try { + const { network, poolAddress, percentageToRemove, walletAddress } = request.body; + + // Validate essential parameters + if (!percentageToRemove || !network || !poolAddress || !walletAddress) { + throw fastify.httpErrors.badRequest('Missing required parameters'); + } + + if (percentageToRemove <= 0 || percentageToRemove > 100) { + throw fastify.httpErrors.badRequest('Percentage to remove must be between 0 and 100'); + } + return await removeLiquidity(fastify, request.body); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + + // Handle insufficient funds errors + if (e.code === 'INSUFFICIENT_FUNDS' || (e.message && e.message.includes('insufficient funds'))) { + throw fastify.httpErrors.badRequest( + 'Insufficient balance to pay for gas fees. Please add more to your wallet.', + ); + } + + throw fastify.httpErrors.internalServerError('Failed to remove liquidity'); + } + }, + ); +}; + +export default removeLiquidityRoute; diff --git a/src/connectors/osmosis/chain-routes/balances.ts b/src/connectors/osmosis/chain-routes/balances.ts new file mode 100755 index 0000000000..16136bf4cb --- /dev/null +++ b/src/connectors/osmosis/chain-routes/balances.ts @@ -0,0 +1,78 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + BalanceRequestType, + BalanceResponseType, + BalanceRequestSchema, + BalanceResponseSchema, +} from '../../../schemas/chain-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function balances(fastify: FastifyInstance, request: BalanceRequestType): Promise { + try { + const network = request.network; + const address = request.address; + const fetchAll = request.fetchAll; + let tokens = request.tokens; + if (fetchAll) { + tokens = []; + } + const osmosis = await Osmosis.getInstance(network); + await osmosis.init(); + const send_tokens = tokens ? tokens : []; + const balances = await osmosis.controller.balances(osmosis, fastify, { + address: address, + tokenSymbols: send_tokens, + }); + + return balances; + } catch (error) { + logger.error(`Error getting balances: ${error.message}`); + throw fastify.httpErrors.internalServerError(`Failed to get balances: ${error.message}`); + } +} + +export const balancesRoute: FastifyPluginAsync = async (fastify) => { + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + fastify.post<{ + Body: BalanceRequestType; + Reply: BalanceResponseType; + }>( + '/balances', + { + schema: { + description: + 'Get Cosmos balances. If no tokens specified or empty array provided, returns native token (OSMO) and only non-zero balances for tokens from the token list. If specific tokens are requested, returns those exact tokens with their balances, including zeros.', + tags: ['/chain/cosmos'], + body: { + ...BalanceRequestSchema, + properties: { + ...BalanceRequestSchema.properties, + network: { + type: 'string', + examples: ['mainnet', 'testnet'], + }, + address: { type: 'string', examples: [walletAddressExample] }, + tokens: { + type: 'array', + items: { type: 'string' }, + description: + 'A list of token symbols or addresses. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances plus the native token.', + examples: [['ATOM', 'OSMO']], + }, + }, + }, + response: { + 200: BalanceResponseSchema, + }, + }, + }, + async (request) => { + return await balances(fastify, request.body); + }, + ); +}; + +export default balancesRoute; diff --git a/src/connectors/osmosis/chain-routes/estimateGas.ts b/src/connectors/osmosis/chain-routes/estimateGas.ts new file mode 100755 index 0000000000..1c373c6ba8 --- /dev/null +++ b/src/connectors/osmosis/chain-routes/estimateGas.ts @@ -0,0 +1,55 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + EstimateGasRequestType, + EstimateGasResponse, + EstimateGasRequestSchema, + EstimateGasResponseSchema, +} from '../../../schemas/chain-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function estimateGas(fastify: FastifyInstance, network: string): Promise { + try { + const osmosis = await Osmosis.getInstance(network); + await osmosis.init(); + return await osmosis.controller.estimateGas(osmosis, fastify); + } catch (error) { + logger.error(`Error estimating gas: ${error.message}`); + throw fastify.httpErrors.internalServerError(`Failed to estimate gas: ${error.message}`); + } +} + +export const estimateGasRoute: FastifyPluginAsync = async (fastify) => { + fastify.post<{ + Body: EstimateGasRequestType; + Reply: EstimateGasResponse; + }>( + '/estimate-gas', + { + schema: { + description: 'Estimate gas prices for Cosmos transactions', + tags: ['/chain/cosmos'], + body: { + ...EstimateGasRequestSchema, + properties: { + ...EstimateGasRequestSchema.properties, + network: { + type: 'string', + examples: ['mainnet', 'testnet'], + }, + }, + }, + response: { + 200: EstimateGasResponseSchema, + }, + }, + }, + async (request) => { + const { network } = request.body; + return await estimateGas(fastify, network); + }, + ); +}; + +export default estimateGasRoute; diff --git a/src/connectors/osmosis/chain-routes/index.ts b/src/connectors/osmosis/chain-routes/index.ts new file mode 100644 index 0000000000..a8d22614ec --- /dev/null +++ b/src/connectors/osmosis/chain-routes/index.ts @@ -0,0 +1,17 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { balancesRoute } from './balances'; +import { estimateGasRoute } from './estimateGas'; +import { pollRoute } from './poll'; +import { statusRoute } from './status'; +import { tokensRoute } from './tokens'; + +export const osmosisChainRoutes: FastifyPluginAsync = async (fastify) => { + await fastify.register(balancesRoute); + await fastify.register(statusRoute); + await fastify.register(estimateGasRoute); + await fastify.register(pollRoute); + await fastify.register(tokensRoute); +}; + +export default osmosisChainRoutes; diff --git a/src/connectors/osmosis/chain-routes/poll.ts b/src/connectors/osmosis/chain-routes/poll.ts new file mode 100755 index 0000000000..f5ce445a73 --- /dev/null +++ b/src/connectors/osmosis/chain-routes/poll.ts @@ -0,0 +1,56 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + PollRequestType, + PollResponseType, + PollRequestSchema, + PollResponseSchema, +} from '../../../schemas/chain-schema'; +import { Osmosis } from '../osmosis'; + +export async function poll(fastify: FastifyInstance, request: PollRequestType): Promise { + try { + const osmosis = await Osmosis.getInstance(request.network); + await osmosis.init(); + return await osmosis.controller.poll(osmosis, request); + } catch (error) { + throw fastify.httpErrors.internalServerError(`Failed to poll transaction: ${error.message}`); + } +} + +export const pollRoute: FastifyPluginAsync = async (fastify) => { + fastify.post<{ + Body: PollRequestType; + Reply: PollResponseType; + }>( + '/poll', + { + schema: { + description: 'Poll Cosmos transaction status', + tags: ['/chain/cosmos'], + body: { + ...PollRequestSchema, + properties: { + ...PollRequestSchema.properties, + network: { + type: 'string', + examples: ['mainnet', 'testnet'], + }, + signature: { + type: 'string', + examples: ['344A0C038C05D1FA938E78828925109879E30C397100BD84D0BA08A463B2FF82'], + }, + }, + }, + response: { + 200: PollResponseSchema, + }, + }, + }, + async (request) => { + return await poll(fastify, request.body); + }, + ); +}; + +export default pollRoute; diff --git a/src/connectors/osmosis/chain-routes/status.ts b/src/connectors/osmosis/chain-routes/status.ts new file mode 100755 index 0000000000..3a70883b75 --- /dev/null +++ b/src/connectors/osmosis/chain-routes/status.ts @@ -0,0 +1,102 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + StatusRequestType, + StatusResponseType, + StatusRequestSchema, + StatusResponseSchema, +} from '../../../schemas/chain-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function status(fastify: FastifyInstance, network: string): Promise { + try { + const osmosis = await Osmosis.getInstance(network); + await osmosis.init(); + + const chain = 'cosmos'; + const rpcProvider = 'osmosis'; + const swapProvider = 'osmosis'; + const nodeURL = osmosis.nodeURL; + const nativeCurrency = osmosis.nativeTokenSymbol; + + // Directly try to get the current block number with a timeout + let currentBlockNumber = 0; + try { + // Set up a timeout promise to prevent hanging on unresponsive nodes + const blockPromise = osmosis.getCurrentBlockNumber(); + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Request timed out')), 5000); + }); + + // Race the block request against the timeout + currentBlockNumber = await Promise.race([blockPromise, timeoutPromise]); + } catch (blockError) { + logger.warn(`Failed to get block number: ${blockError.message}`); + } + + return { + chain, + network, + rpcUrl: nodeURL, + currentBlockNumber, + nativeCurrency, + rpcProvider, + swapProvider, + }; + } catch (error) { + logger.error(`Error getting cosmos status: ${error.message}`); + fastify.httpErrors.internalServerError(`Failed to get cosmos status: ${error.message}`); + } +} + +export const statusRoute: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: StatusRequestType; + Reply: StatusResponseType; + }>( + '/status', + { + schema: { + description: 'Get cosmos chain status', + tags: ['/chain/cosmos'], + querystring: { + ...StatusRequestSchema, + properties: { + ...StatusRequestSchema.properties, + network: { + type: 'string', + examples: ['mainnet', 'testnet'], + }, + }, + }, + response: { + 200: StatusResponseSchema, + }, + }, + }, + async (request, reply) => { + const { network } = request.query; + try { + // This will handle node timeout internally + return await status(fastify, network); + } catch (error) { + // This will catch any other unexpected errors + logger.error(`Error in cosmos status endpoint: ${error.message}`); + reply.status(500); + // Return a minimal valid response + return { + chain: 'cosmos', + network, + rpcUrl: 'unavailable', + currentBlockNumber: 0, + nativeCurrency: 'ATOM', + rpcProvider: 'unavailable', + swapProvider: 'unavailable', + }; + } + }, + ); +}; + +export default statusRoute; diff --git a/src/connectors/osmosis/chain-routes/tokens.ts b/src/connectors/osmosis/chain-routes/tokens.ts new file mode 100755 index 0000000000..d5514b7287 --- /dev/null +++ b/src/connectors/osmosis/chain-routes/tokens.ts @@ -0,0 +1,64 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + TokensRequestType, + TokensResponseType, + TokensRequestSchema, + TokensResponseSchema, +} from '../../../schemas/chain-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function tokens(fastify: FastifyInstance, request: TokensRequestType): Promise { + try { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = await osmosis.controller.getTokens(osmosis, request); + return response; + } catch (error) { + logger.error(`Error getting tokens: ${error.message}`); + throw fastify.httpErrors.internalServerError(`Failed to estimate gas: ${error.message}`); + } +} + +export const tokensRoute: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: TokensRequestType; + Reply: TokensResponseType; + }>( + '/tokens', + { + schema: { + description: 'Get Cosmos/Osmosis tokens', + tags: ['/chain/cosmos'], + querystring: { + ...TokensRequestSchema, + properties: { + ...TokensRequestSchema.properties, + network: { + type: 'string', + examples: ['mainnet', 'testnet'], + }, + }, + }, + response: { + 200: TokensResponseSchema, + }, + }, + }, + async (request, reply) => { + try { + return await tokens(fastify, request.query); + } catch (error) { + logger.error(`Error handling Osmosis tokens request: ${error.message}`); + reply.status(500); + return { tokens: [] }; + } + }, + ); +}; + +export default tokensRoute; diff --git a/src/connectors/osmosis/clmm-routes/addLiquidity.ts b/src/connectors/osmosis/clmm-routes/addLiquidity.ts new file mode 100755 index 0000000000..8715b505ce --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/addLiquidity.ts @@ -0,0 +1,114 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { + AddLiquidityRequestType as CLMMAddLiquidityRequestType, + AddLiquidityResponseType as CLMMAddLiquidityResponseType, + AddLiquidityRequest as CLMMAddLiquidityRequest, + AddLiquidityResponse as CLMMAddLiquidityResponse, +} from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function addLiquidity( + fastify: any, + req: CLMMAddLiquidityRequestType, +): Promise { + let networkToUse = req.network ? req.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + + const response: CLMMAddLiquidityResponseType = await osmosis.controller.addLiquidityCLMM(osmosis, fastify, req); + return response; +} + +export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { + await fastify.register(require('@fastify/sensible')); + + fastify.post<{ + Body: CLMMAddLiquidityRequestType; + Reply: CLMMAddLiquidityResponseType; + }>( + '/add-liquidity', + { + schema: { + description: 'Add liquidity to an existing Osmosis CL position', + tags: ['uniswap/clmm'], + body: { + ...CLMMAddLiquidityRequest, + properties: { + ...CLMMAddLiquidityRequest.properties, + network: { type: 'string', default: 'base' }, + walletAddress: { type: 'string', examples: ['0x...'] }, + positionAddress: { + type: 'string', + description: 'Position NFT token ID', + }, + baseTokenAmount: { type: 'number', examples: [0.1] }, + quoteTokenAmount: { type: 'number', examples: [200] }, + slippagePct: { type: 'number', examples: [1] }, + }, + }, + response: { + 200: CLMMAddLiquidityResponse, + }, + }, + }, + async (request) => { + try { + const { + network, + walletAddress: requestedWalletAddress, + positionAddress, + baseTokenAmount, + quoteTokenAmount, + slippagePct, + } = request.body; + + // Validate essential parameters + if ( + !network || + !positionAddress || + !slippagePct || + (baseTokenAmount === undefined && quoteTokenAmount === undefined) + ) { + throw fastify.httpErrors.badRequest('Missing required parameters'); + } + + // Get wallet address - either from request or first available + let walletAddress = requestedWalletAddress; + if (!walletAddress) { + walletAddress = await Osmosis.getFirstWalletAddress(); + if (!walletAddress) { + throw fastify.httpErrors.badRequest('No wallet address provided and no wallets found.'); + } + logger.info(`Using first available wallet address: ${walletAddress}`); + } + + return await addLiquidity(fastify, request.body); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + + // Handle specific user-actionable errors + if (e.message && e.message.includes('Insufficient allowance')) { + throw fastify.httpErrors.badRequest(e.message); + } + + // Handle insufficient funds errors + if (e.code === 'INSUFFICIENT_FUNDS' || (e.message && e.message.includes('insufficient funds'))) { + throw fastify.httpErrors.badRequest( + 'Insufficient balance to pay for gas fees. Please add more to your wallet.', + ); + } + + throw fastify.httpErrors.internalServerError('Failed to add liquidity'); + } + }, + ); +}; + +export default addLiquidityRoute; diff --git a/src/connectors/osmosis/clmm-routes/closePosition.ts b/src/connectors/osmosis/clmm-routes/closePosition.ts new file mode 100644 index 0000000000..3beb329586 --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/closePosition.ts @@ -0,0 +1,82 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + ClosePositionRequest as CLMMClosePositionRequest, + ClosePositionRequestType as CLMMClosePositionRequestType, + ClosePositionResponse as CLMMClosePositionResponse, + ClosePositionResponseType as CLMMClosePositionResponseType, +} from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function closePosition( + fastify: FastifyInstance, + request: CLMMClosePositionRequestType, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = await osmosis.controller.closePositionCLMM(osmosis, fastify, request); + return response; +} + +export const closePositionRoute: FastifyPluginAsync = async (fastify) => { + fastify.post<{ + Body: CLMMClosePositionRequestType; + Reply: CLMMClosePositionResponseType; + }>( + '/close-position', + { + schema: { + description: 'Close an Osmosis CL position by removing all liquidity and collecting fees', + tags: ['osmosis/connector'], + body: { + ...CLMMClosePositionRequest, + properties: { + ...CLMMClosePositionRequest.properties, + network: { type: 'string', default: 'base' }, + walletAddress: { type: 'string', examples: ['0x...'] }, + positionAddress: { + type: 'string', + description: 'Position NFT token ID', + }, + }, + }, + response: { + 200: CLMMClosePositionResponse, + }, + }, + }, + async (request) => { + try { + // Validate essential parameters + const { positionAddress } = request.body; + if (!positionAddress) { + throw fastify.httpErrors.badRequest('Missing required parameters'); + } + + return (await closePosition(fastify, request.body)) as CLMMClosePositionResponseType; + } catch (e) { + logger.error(`Error in pool-info route: ${e.message}`); + if (e.stack) { + logger.debug(`Stack trace: ${e.stack}`); + } + + // Return appropriate error based on the error message + if (e.statusCode) { + throw e; // Already a formatted Fastify error + } else if (e.message && e.message.includes('invalid address')) { + throw fastify.httpErrors.badRequest(`Invalid pool address`); + } else if (e.message && e.message.includes('not found')) { + throw fastify.httpErrors.notFound(e.message); + } else { + throw fastify.httpErrors.internalServerError(`Failed to fetch pool info: ${e.message}`); + } + } + }, + ); +}; + +export default closePositionRoute; diff --git a/src/connectors/osmosis/clmm-routes/collectFees.ts b/src/connectors/osmosis/clmm-routes/collectFees.ts new file mode 100644 index 0000000000..02a177ecdf --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/collectFees.ts @@ -0,0 +1,85 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + CollectFeesRequestType as CLMMCollectFeesRequestType, + CollectFeesResponseType as CLMMCollectFeesResponseType, + CollectFeesRequest as CLMMCollectFeesRequest, + CollectFeesResponse as CLMMCollectFeesResponse, +} from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function collectFees( + fastify: FastifyInstance, + request: CLMMCollectFeesRequestType, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = await osmosis.controller.collectFees(osmosis, fastify, request); + return response; +} + +export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + fastify.post<{ + Body: CLMMCollectFeesRequestType; + Reply: CLMMCollectFeesResponseType; + }>( + '/collect-fees', + { + schema: { + description: 'Collect fees from an Osmosis CL position', + tags: ['osmosis/connector'], + body: { + ...CLMMCollectFeesRequest, + properties: { + ...CLMMCollectFeesRequest.properties, + network: { type: 'string', default: 'base' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + positionAddress: { + type: 'string', + description: 'Position address', + examples: ['1234'], + }, + }, + }, + response: { + 200: CLMMCollectFeesResponse, + }, + }, + }, + async (request) => { + try { + // Validate essential parameters + const { positionAddress } = request.body; + if (!positionAddress) { + throw fastify.httpErrors.badRequest('Missing required parameters'); + } + + return (await collectFees(fastify, request.body)) as CLMMCollectFeesResponseType; + } catch (e) { + logger.error(`Error in pool-info route: ${e.message}`); + if (e.stack) { + logger.debug(`Stack trace: ${e.stack}`); + } + + // Return appropriate error based on the error message + if (e.statusCode) { + throw e; // Already a formatted Fastify error + } else if (e.message && e.message.includes('invalid address')) { + throw fastify.httpErrors.badRequest(`Invalid pool address`); + } else if (e.message && e.message.includes('not found')) { + throw fastify.httpErrors.notFound(e.message); + } else { + throw fastify.httpErrors.internalServerError(`Failed to fetch pool info: ${e.message}`); + } + } + }, + ); +}; + +export default collectFeesRoute; diff --git a/src/connectors/osmosis/clmm-routes/executeSwap.ts b/src/connectors/osmosis/clmm-routes/executeSwap.ts new file mode 100755 index 0000000000..c856c7358b --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/executeSwap.ts @@ -0,0 +1,89 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { ExecuteSwapRequestType, ExecuteSwapResponseType, ExecuteSwapResponse } from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; +import { executeSwap } from '../osmosis.swap'; + +export const executeSwapRoute: FastifyPluginAsync = async (fastify, _options) => { + // Import the httpErrors plugin to ensure it's available + await fastify.register(require('@fastify/sensible')); + + // Get first wallet address for example + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + // Get available networks from Osmosis configuration (same method as chain.routes.ts) + const { ConfigManagerV2 } = require('../../../services/config-manager-v2'); + const osmosisNetworks = ['testnet', 'mainnet']; + + fastify.post<{ + Body: ExecuteSwapRequestType; + Reply: ExecuteSwapResponseType; + }>( + '/execute-swap', + { + schema: { + description: 'Execute a swap using Osmosis Order Router', + tags: ['osmosis'], + body: { + type: 'object', + properties: { + network: { + type: 'string', + default: 'mainnet', + enum: osmosisNetworks, + }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + baseToken: { type: 'string', examples: ['WETH'] }, + quoteToken: { type: 'string', examples: ['USDC'] }, + amount: { type: 'number', examples: [0.001] }, + side: { type: 'string', enum: ['BUY', 'SELL'], examples: ['SELL'] }, + slippagePct: { type: 'number', examples: [0.5] }, + }, + required: ['walletAddress', 'baseToken', 'quoteToken', 'amount', 'side'], + }, + response: { + 200: ExecuteSwapResponse, + }, + }, + }, + async (request, reply) => { + try { + // Log the request parameters for debugging + logger.info(`Received execute-swap request: ${JSON.stringify(request.body)}`); + const { + network, + walletAddress, + baseToken: baseTokenSymbol, + quoteToken: quoteTokenSymbol, + amount, + side, + slippagePct, + poolAddress, + } = request.body; + + // Validate essential parameters + if (!(poolAddress || !baseTokenSymbol || !quoteTokenSymbol) || !amount || !side || !network || !walletAddress) { + logger.error('Missing required parameters in request'); + return reply.badRequest('Missing required parameters'); + } + + const executeSwapResponse = await executeSwap(fastify, request.body, 'clmm'); + return executeSwapResponse; + } catch (e) { + logger.error(`Execute swap error: ${e.message}`); + if (e.stack) { + logger.debug(`Error stack: ${e.stack}`); + } + + if (e.code === 'UNPREDICTABLE_GAS_LIMIT') { + return reply.badRequest('Transaction failed: Insufficient funds or gas estimation error'); + } + + return reply.internalServerError(`Failed to execute swap: ${e.message}`); + } + }, + ); +}; + +export default executeSwapRoute; diff --git a/src/connectors/osmosis/clmm-routes/fetchPools.ts b/src/connectors/osmosis/clmm-routes/fetchPools.ts new file mode 100644 index 0000000000..271955e49b --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/fetchPools.ts @@ -0,0 +1,69 @@ +import { Type } from '@sinclair/typebox'; +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { PoolInfoSchema, FetchPoolsRequestType } from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; +import { FetchPoolsRequest, SerializableExtendedPool } from '../osmosis.types'; + +export async function fetchPools( + fastify: FastifyInstance, + request: FetchPoolsRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = await osmosis.controller.fetchPoolsForTokens(osmosis, fastify, request, poolType); + return response; +} + +export const fetchPoolsRoute: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: FetchPoolsRequestType; + Reply: Record; + }>( + '/fetch-pools', + { + schema: { + description: 'Fetch info about Osmosis pools', + tags: ['/connector/osmosis'], + querystring: FetchPoolsRequest, + response: { + 200: Type.Array(PoolInfoSchema), + }, + }, + }, + async (request): Promise => { + try { + const { tokenA, tokenB } = request.query; + + if (!tokenA || !tokenB) { + throw fastify.httpErrors.badRequest('Both baseToken and quoteToken must be provided'); + } + + return await fetchPools(fastify, request.body, 'amm'); + } catch (e) { + logger.error(`Error in pool-info route: ${e.message}`); + if (e.stack) { + logger.debug(`Stack trace: ${e.stack}`); + } + + // Return appropriate error based on the error message + if (e.statusCode) { + throw e; // Already a formatted Fastify error + } else if (e.message && e.message.includes('invalid address')) { + throw fastify.httpErrors.badRequest(`Invalid pool address`); + } else if (e.message && e.message.includes('not found')) { + throw fastify.httpErrors.notFound(e.message); + } else { + throw fastify.httpErrors.internalServerError(`Failed to fetch pool info: ${e.message}`); + } + } + }, + ); +}; + +export default fetchPoolsRoute; diff --git a/src/connectors/osmosis/clmm-routes/index.ts b/src/connectors/osmosis/clmm-routes/index.ts new file mode 100644 index 0000000000..3cccdfe4af --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/index.ts @@ -0,0 +1,29 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { addLiquidityRoute } from './addLiquidity'; +import { closePositionRoute } from './closePosition'; +import { collectFeesRoute } from './collectFees'; +import { executeSwapRoute } from './executeSwap'; +import { openPositionRoute } from './openPosition'; +import { poolInfoRoute } from './poolInfo'; +import { positionInfoRoute } from './positionInfo'; +import { positionsOwnedRoute } from './positionsOwned'; +import { quotePositionRoute } from './quotePosition'; +import { quoteSwapRoute } from './quoteSwap'; +import { removeLiquidityRoute } from './removeLiquidity'; + +export const osmosisClmmRoutes: FastifyPluginAsync = async (fastify) => { + await fastify.register(poolInfoRoute); + await fastify.register(positionInfoRoute); + await fastify.register(positionsOwnedRoute); + await fastify.register(quotePositionRoute); + await fastify.register(quoteSwapRoute); + await fastify.register(executeSwapRoute); + await fastify.register(openPositionRoute); + await fastify.register(addLiquidityRoute); + await fastify.register(removeLiquidityRoute); + await fastify.register(collectFeesRoute); + await fastify.register(closePositionRoute); +}; + +export default osmosisClmmRoutes; diff --git a/src/connectors/osmosis/clmm-routes/openPosition.ts b/src/connectors/osmosis/clmm-routes/openPosition.ts new file mode 100644 index 0000000000..7413904c6c --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/openPosition.ts @@ -0,0 +1,88 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + OpenPositionRequest as CLMMOpenPositionRequest, + OpenPositionRequestType as CLMMOpenPositionRequestType, + OpenPositionResponse as CLMMOpenPositionResponse, + OpenPositionResponseType as CLMMOpenPositionResponseType, +} from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function openPosition( + fastify: FastifyInstance, + request: CLMMOpenPositionRequestType, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = await osmosis.controller.openPositionCLMM(osmosis, fastify, request); + return response; +} + +export const openPositionRoute: FastifyPluginAsync = async (fastify) => { + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + fastify.post<{ + Body: CLMMOpenPositionRequestType; + Reply: CLMMOpenPositionResponseType; + }>( + '/open-position', + { + schema: { + description: 'Open a new liquidity position in an Osmosis CL Pool', + tags: ['osmosis/connector'], + body: { + ...CLMMOpenPositionRequest, + properties: { + ...CLMMOpenPositionRequest.properties, + network: { type: 'string', default: 'base' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + lowerPrice: { type: 'number', examples: [1000] }, + upperPrice: { type: 'number', examples: [4000] }, + poolAddress: { type: 'string', examples: [''] }, + baseToken: { type: 'string', examples: ['ION'] }, + quoteToken: { type: 'string', examples: ['OSMO'] }, + baseTokenAmount: { type: 'number', examples: [0.001] }, + quoteTokenAmount: { type: 'number', examples: [3] }, + slippagePct: { type: 'number', examples: [1] }, + }, + }, + response: { + 200: CLMMOpenPositionResponse, + }, + }, + }, + async (request) => { + try { + const { lowerPrice, upperPrice, baseTokenAmount, quoteTokenAmount } = request.body; + // Validate essential parameters + if (!lowerPrice || !upperPrice || (baseTokenAmount === undefined && quoteTokenAmount === undefined)) { + throw fastify.httpErrors.badRequest('Missing required parameters'); + } + + return (await openPosition(fastify, request.body)) as CLMMOpenPositionResponseType; + } catch (e) { + logger.error(`Error in pool-info route: ${e.message}`); + if (e.stack) { + logger.debug(`Stack trace: ${e.stack}`); + } + + // Return appropriate error based on the error message + if (e.statusCode) { + throw e; // Already a formatted Fastify error + } else if (e.message && e.message.includes('invalid address')) { + throw fastify.httpErrors.badRequest(`Invalid pool address`); + } else if (e.message && e.message.includes('not found')) { + throw fastify.httpErrors.notFound(e.message); + } else { + throw fastify.httpErrors.internalServerError(`Failed to fetch pool info: ${e.message}`); + } + } + }, + ); +}; + +export default openPositionRoute; diff --git a/src/connectors/osmosis/clmm-routes/poolInfo.ts b/src/connectors/osmosis/clmm-routes/poolInfo.ts new file mode 100644 index 0000000000..5f4a166586 --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/poolInfo.ts @@ -0,0 +1,90 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + GetPoolInfoRequest, + PoolInfoSchema, + PoolInfo as AMMPoolInfo, + GetPoolInfoRequestType as AMMGetPoolInfoRequestType, +} from '../../../schemas/amm-schema'; +import { + PoolInfo as CLMMPoolInfo, + GetPoolInfoRequestType as CLMMGetPoolInfoRequestType, +} from '../../../schemas/clmm-schema'; +import { ConfigManagerV2 } from '../../../services/config-manager-v2'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; +const osmosisNetworks = ['testnet', 'mainnet']; + +export async function poolInfo( + fastify: FastifyInstance, + request: AMMGetPoolInfoRequestType | CLMMGetPoolInfoRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = (await osmosis.controller.poolInfoRequest(osmosis, fastify, request, poolType)) as CLMMPoolInfo; + return response; +} + +export const poolInfoRoute: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: CLMMGetPoolInfoRequestType; + Reply: Record; + }>( + '/pool-info', + { + schema: { + description: 'Get CLMM pool information from Osmosis', + tags: ['osmosis/connector'], + querystring: { + ...GetPoolInfoRequest, + properties: { + network: { + type: 'string', + default: 'mainnet', + enum: osmosisNetworks, + }, + poolAddress: { + type: 'string', + examples: ['osmo146zct0tppdd4yyrdpn8u8j82yvhwvpx23pmy7yh45xj0ttya305s2edl6v'], + }, + }, + }, + response: { + 200: PoolInfoSchema, + }, + }, + }, + async (request): Promise => { + try { + const { poolAddress } = request.query; + if (!poolAddress) { + throw fastify.httpErrors.badRequest('Pool address must be provided'); + } + + return (await poolInfo(fastify, request.query, 'clmm')) as CLMMPoolInfo; + } catch (e) { + logger.error(`Error in pool-info route: ${e.message}`); + if (e.stack) { + logger.debug(`Stack trace: ${e.stack}`); + } + + // Return appropriate error based on the error message + if (e.statusCode) { + throw e; // Already a formatted Fastify error + } else if (e.message && e.message.includes('invalid address')) { + throw fastify.httpErrors.badRequest(`Invalid pool address`); + } else if (e.message && e.message.includes('not found')) { + throw fastify.httpErrors.notFound(e.message); + } else { + throw fastify.httpErrors.internalServerError(`Failed to fetch pool info: ${e.message}`); + } + } + }, + ); +}; + +export default poolInfoRoute; diff --git a/src/connectors/osmosis/clmm-routes/positionInfo.ts b/src/connectors/osmosis/clmm-routes/positionInfo.ts new file mode 100644 index 0000000000..88f5e27b00 --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/positionInfo.ts @@ -0,0 +1,78 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { + PositionInfo as AMMPositionInfo, + GetPositionInfoRequestType as AMMGetPositionInfoRequestType, +} from '../../../schemas/amm-schema'; +import { + PositionInfo as CLMMPositionInfo, + PositionInfoSchema as CLMMPositionInfoSchema, + GetPositionInfoRequestType as CLMMGetPositionInfoRequestType, + GetPositionInfoRequest as CLMMGetPositionInfoRequest, +} from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function positionInfo( + fastify: FastifyInstance, + request: AMMGetPositionInfoRequestType | CLMMGetPositionInfoRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = await osmosis.controller.poolPosition(osmosis, fastify, request, poolType); + return response; +} + +export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + fastify.get<{ + Querystring: CLMMGetPositionInfoRequestType; + Reply: CLMMPositionInfo; + }>( + '/position-info', + { + schema: { + description: 'Get position information for a osmosis CLMM pool', + tags: ['osmosis/connector'], + querystring: { + ...CLMMGetPositionInfoRequest, + properties: { + network: { type: 'string', default: 'base' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + poolAddress: { + type: 'string', + examples: [''], + }, + }, + }, + response: { + 200: CLMMPositionInfoSchema, + }, + }, + }, + async (request) => { + try { + // Validate essential parameters + const { positionAddress } = request.query; + if (!positionAddress) { + throw fastify.httpErrors.badRequest('Pool address must be provided'); + } + + return (await positionInfo(fastify, request.query, 'clmm')) as CLMMPositionInfo; + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw fastify.httpErrors.internalServerError('Failed to get position info'); + } + }, + ); +}; + +export default positionInfoRoute; diff --git a/src/connectors/osmosis/clmm-routes/positionsOwned.ts b/src/connectors/osmosis/clmm-routes/positionsOwned.ts new file mode 100644 index 0000000000..f9ebef51cd --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/positionsOwned.ts @@ -0,0 +1,90 @@ +import { Type, Static } from '@sinclair/typebox'; +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { PositionInfoSchema as AMMPositionInfoSchema } from '../../../schemas/amm-schema'; +import { + PositionInfo as CLMMPositionInfo, + PositionInfoSchema as CLMMPositionInfoSchema, + GetPositionInfoRequest as CLMMGetPositionInfoRequest, +} from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +const PositionsOwnedRequest = Type.Object({ + network: Type.Optional(Type.String({ examples: ['mainnet'], default: 'mainnet' })), + walletAddress: Type.String({ examples: [''] }), +}); +type PositionsOwnedRequestType = Static; + +const AMMAllPositionsOwnedResponse = Type.Array(AMMPositionInfoSchema); +const CLMMAllPositionsOwnedResponse = Type.Array(CLMMPositionInfoSchema); +type AMMAllPositionsOwnedResponseType = Static; +type CLMMAllPositionsOwnedResponseType = Static; + +export async function positionsOwned( + fastify: FastifyInstance, + request: PositionsOwnedRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response = (await osmosis.controller.allPoolPositions( + osmosis, + fastify, + request.walletAddress, + poolType, + )) as CLMMAllPositionsOwnedResponseType; + return response; +} + +export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + fastify.get<{ + Querystring: typeof PositionsOwnedRequest.static; + Reply: CLMMAllPositionsOwnedResponseType; + }>( + '/positions-owned', + { + schema: { + description: 'Get all CLMM positions for wallet address. Warning: Spams RPC to do so (only way for CL pools).', + tags: ['osmosis/connector'], + querystring: { + ...CLMMGetPositionInfoRequest, + properties: { + network: { type: 'string', default: 'mainnet' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + }, + }, + response: { + 200: CLMMPositionInfoSchema, + }, + }, + }, + async (request) => { + try { + const { walletAddress: requestedWalletAddress } = request.query; + + // Validate essential parameters + if (!requestedWalletAddress) { + throw fastify.httpErrors.badRequest( + 'Either pool address or both base token and quote token must be provided', + ); + } + + return (await positionsOwned(fastify, request.query, 'clmm')) as unknown as CLMMPositionInfo[]; + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw fastify.httpErrors.internalServerError('Failed to get position info'); + } + }, + ); +}; + +export default positionsOwnedRoute; diff --git a/src/connectors/osmosis/clmm-routes/quotePosition.ts b/src/connectors/osmosis/clmm-routes/quotePosition.ts new file mode 100644 index 0000000000..f3ecef2b84 --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/quotePosition.ts @@ -0,0 +1,78 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { + QuotePositionRequestType, + QuotePositionRequest, + QuotePositionResponseType, + QuotePositionResponse, +} from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +const BASE_TOKEN_AMOUNT = 0.001; +const QUOTE_TOKEN_AMOUNT = 3; +const LOWER_PRICE_BOUND = 2000; +const UPPER_PRICE_BOUND = 4000; +const POOL_ADDRESS_EXAMPLE = 'osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6'; + +export const quotePositionRoute: FastifyPluginAsync = async (fastify) => { + fastify.get<{ + Querystring: QuotePositionRequestType; + Reply: QuotePositionResponseType; + }>( + '/quote-position', + { + schema: { + description: 'Get a quote for opening a position on Osmosis CL', + tags: ['/connector/osmosis'], + querystring: { + ...QuotePositionRequest, + properties: { + ...QuotePositionRequest.properties, + network: { type: 'string', default: 'base', examples: ['base'] }, + lowerPrice: { type: 'number', examples: [LOWER_PRICE_BOUND] }, + upperPrice: { type: 'number', examples: [UPPER_PRICE_BOUND] }, + poolAddress: { + type: 'string', + default: POOL_ADDRESS_EXAMPLE, + examples: [POOL_ADDRESS_EXAMPLE], + }, + baseTokenAmount: { type: 'number', examples: [BASE_TOKEN_AMOUNT] }, + quoteTokenAmount: { type: 'number', examples: [QUOTE_TOKEN_AMOUNT] }, + }, + }, + response: { + 200: QuotePositionResponse, + }, + }, + }, + async (request) => { + try { + const { network, lowerPrice, upperPrice, poolAddress, baseTokenAmount, quoteTokenAmount } = request.query; + + const networkToUse = network; + // Validate essential parameters + if ( + !lowerPrice || + !upperPrice || + !poolAddress || + (baseTokenAmount === undefined && quoteTokenAmount === undefined) + ) { + throw fastify.httpErrors.badRequest('Missing required parameters'); + } + + // Get osmosis and cosmos instances + const osmosis = await Osmosis.getInstance(networkToUse); + return await osmosis.QuotePositionCLMM(request.query); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + throw fastify.httpErrors.internalServerError('Failed to quote position'); + } + }, + ); +}; + +export default quotePositionRoute; diff --git a/src/connectors/osmosis/clmm-routes/quoteSwap.ts b/src/connectors/osmosis/clmm-routes/quoteSwap.ts new file mode 100755 index 0000000000..eb35a42a7c --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/quoteSwap.ts @@ -0,0 +1,136 @@ +import { FastifyPluginAsync, FastifyInstance } from 'fastify'; + +import { QuoteSwapResponseType, QuoteSwapResponse, QuoteSwapRequestType } from '../../../schemas/clmm-schema'; +import { ConfigManagerV2 } from '../../../services/config-manager-v2'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function osmosisQuoteSwap( + fastify: FastifyInstance, + request: QuoteSwapRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + + const response: QuoteSwapResponseType = await osmosis.controller.quoteSwap(osmosis, fastify, request, poolType); + return response; +} + +export const quoteSwapRoute: FastifyPluginAsync = async (fastify, _options) => { + // Import the httpErrors plugin to ensure it's available + await fastify.register(require('@fastify/sensible')); + + // Get first wallet address for example + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + // Get available networks from osmosis configuration (same method as chain.routes.ts) + const osmosisNetworks = ['testnet', 'mainnet']; + + fastify.get<{ + Querystring: QuoteSwapRequestType; + Reply: QuoteSwapResponseType; + }>( + '/quote-swap', + { + schema: { + description: 'Get a swap quote using Osmosis router', + tags: ['/connectors/osmosis'], + querystring: { + type: 'object', + properties: { + network: { + type: 'string', + default: 'mainnet', + enum: osmosisNetworks, + }, + baseToken: { type: 'string', examples: ['WETH'] }, + quoteToken: { type: 'string', examples: ['USDC'] }, + amount: { type: 'number', examples: [0.001] }, + side: { type: 'string', enum: ['BUY', 'SELL'], examples: ['SELL'] }, + slippagePct: { type: 'number', examples: [0.5] }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + }, + required: ['baseToken', 'quoteToken', 'amount', 'side'], + }, + response: { + 200: QuoteSwapResponse, + }, + }, + }, + async (request, reply) => { + try { + // Log the request parameters for debugging + logger.info(`Received quote-swap request: ${JSON.stringify(request.query)}`); + + const { + network, + baseToken: baseTokenSymbol, + quoteToken: quoteTokenSymbol, + amount, + side, + slippagePct, + } = request.query; + + // Validate essential parameters + if (!baseTokenSymbol || !quoteTokenSymbol || !amount || !side || !network || !slippagePct) { + logger.error('Missing required parameters in request'); + return reply.badRequest('Missing required parameters'); + } + + try { + // Use our shared quote function + const quoteResult = await osmosisQuoteSwap(fastify, request.query, 'clmm'); + + // Return only the data needed for the API response + return { + slippagePct: quoteResult.slippagePct, + poolAddress: quoteResult.poolAddress, + tokenIn: request.query.baseToken, + tokenOut: request.query.quoteToken, + amountIn: quoteResult.amountIn, + amountOut: quoteResult.amountOut, + price: quoteResult.price, + minAmountOut: quoteResult.minAmountOut, + maxAmountIn: quoteResult.maxAmountIn, + priceImpactPct: quoteResult.priceImpactPct, + }; + } catch (error) { + // If the error already has a status code, it's a Fastify HTTP error + if (error.statusCode) { + throw error; + } + + // Log more detailed information about the error + logger.error(`Router error: ${error.message}`); + if (error.stack) { + logger.debug(`Error stack: ${error.stack}`); + } + + // Check if there's any additional error details + if (error.innerError) { + logger.error(`Inner error: ${JSON.stringify(error.innerError)}`); + } + + // Check if it's a specific error type from the Alpha Router + if (error.name === 'SwapRouterError') { + logger.error(`SwapRouterError details: ${JSON.stringify(error)}`); + } + + return reply.badRequest(`Failed to get quote with router: ${error.message}`); + } + } catch (e) { + logger.error(`Quote swap error: ${e.message}`); + if (e.stack) { + logger.debug(`Error stack: ${e.stack}`); + } + return reply.internalServerError(`Failed to get quote: ${e.message}`); + } + }, + ); +}; + +export default quoteSwapRoute; diff --git a/src/connectors/osmosis/clmm-routes/removeLiquidity.ts b/src/connectors/osmosis/clmm-routes/removeLiquidity.ts new file mode 100755 index 0000000000..c6210e8f4b --- /dev/null +++ b/src/connectors/osmosis/clmm-routes/removeLiquidity.ts @@ -0,0 +1,94 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { + RemoveLiquidityRequestType as CLMMRemoveLiquidityRequestType, + RemoveLiquidityResponseType as CLMMRemoveLiquidityResponseType, + RemoveLiquidityRequest as CLMMRemoveLiquidityRequest, + RemoveLiquidityResponse as CLMMRemoveLiquidityResponse, +} from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; + +export async function removeLiquidity( + fastify: any, + req: CLMMRemoveLiquidityRequestType, +): Promise { + let networkToUse = req.network ? req.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + + const response: CLMMRemoveLiquidityResponseType = await osmosis.controller.removeLiquidityCLMM(osmosis, fastify, req); + return response; +} + +export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { + await fastify.register(require('@fastify/sensible')); + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + fastify.post<{ + Body: CLMMRemoveLiquidityRequestType; + Reply: CLMMRemoveLiquidityResponseType; + }>( + '/remove-liquidity', + { + schema: { + description: 'Remove liquidity from an Osmosis CL Position', + tags: ['osmosis/connector'], + body: { + ...CLMMRemoveLiquidityRequest, + properties: { + ...CLMMRemoveLiquidityRequest.properties, + network: { type: 'string', default: 'base' }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + positionAddress: { + type: 'string', + description: 'Position address', + examples: ['1234'], + }, + percentageToRemove: { + type: 'number', + minimum: 0, + maximum: 100, + examples: [50], + }, + }, + }, + response: { + 200: CLMMRemoveLiquidityResponse, + }, + }, + }, + async (request) => { + try { + // Validate essential parameters + const { positionAddress, percentageToRemove } = request.body; + if (!positionAddress || percentageToRemove === undefined) { + throw fastify.httpErrors.badRequest('Missing required parameters'); + } + + if (percentageToRemove < 0 || percentageToRemove > 100) { + throw fastify.httpErrors.badRequest('Percentage to remove must be between 0 and 100'); + } + return await removeLiquidity(fastify, request.body); + } catch (e) { + logger.error(e); + if (e.statusCode) { + throw e; + } + + // Handle insufficient funds errors + if (e.code === 'INSUFFICIENT_FUNDS' || (e.message && e.message.includes('insufficient funds'))) { + throw fastify.httpErrors.badRequest( + 'Insufficient balance to pay for gas fees. Please add more to your wallet.', + ); + } + + throw fastify.httpErrors.internalServerError('Failed to remove liquidity'); + } + }, + ); +}; + +export default removeLiquidityRoute; diff --git a/src/connectors/osmosis/osmosis.apr.ts b/src/connectors/osmosis/osmosis.apr.ts new file mode 100755 index 0000000000..a8af00649e --- /dev/null +++ b/src/connectors/osmosis/osmosis.apr.ts @@ -0,0 +1,31 @@ +import { calcPoolAprs as _calcPoolAprs } from '@osmonauts/math'; + +import { CalcPoolAprsParams } from './osmosis.types'; + +// need to pass this tokenList from osmosis.ts... +export const calcPoolAprs = ({ + activeGauges, + pool, + prices, + superfluidPools, + aprSuperfluid, + lockupDurations, + volume7d, + swapFee, + lockup = '14', + includeNonPerpetual = true, +}: CalcPoolAprsParams) => { + return _calcPoolAprs({ + activeGauges, + pool, + assets: [], + prices, + superfluidPools, + aprSuperfluid, + lockupDurations, + volume7d, + swapFee, + lockup, + includeNonPerpetual, + }); +}; diff --git a/src/connectors/osmosis/osmosis.chain.routes.ts b/src/connectors/osmosis/osmosis.chain.routes.ts new file mode 100755 index 0000000000..cb6796d3a5 --- /dev/null +++ b/src/connectors/osmosis/osmosis.chain.routes.ts @@ -0,0 +1,26 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { balancesRoute } from './chain-routes/balances'; +import { estimateGasRoute } from './chain-routes/estimateGas'; +import { pollRoute } from './chain-routes/poll'; +import { statusRoute } from './chain-routes/status'; +import { tokensRoute } from './chain-routes/tokens'; + +// Register the type declaration needed for Fastify schema tags +declare module 'fastify' { + interface FastifySchema { + tags?: readonly string[]; + description?: string; + } +} + +export const osmosisChainRoutes: FastifyPluginAsync = async (fastify) => { + // Register all the route handlers + fastify.register(statusRoute); + fastify.register(tokensRoute); + fastify.register(balancesRoute); + fastify.register(pollRoute); + fastify.register(estimateGasRoute); +}; + +export default osmosisChainRoutes; diff --git a/src/connectors/osmosis/osmosis.config.ts b/src/connectors/osmosis/osmosis.config.ts new file mode 100755 index 0000000000..31a633bac9 --- /dev/null +++ b/src/connectors/osmosis/osmosis.config.ts @@ -0,0 +1,66 @@ +import { ConfigManagerV2 } from '../../services/config-manager-v2'; + +// Left in for logic to support JSON from URL but no longer used (moved to token service) +// tokenListType: URL +// tokenListSource: https://raw.githubusercontent.com/osmosis-labs/assetlists/refs/heads/main/osmosis-1/generated/frontend/assetlist.json +// tokenListType: URL +// tokenListSource: https://github.com/osmosis-labs/assetlists/raw/refs/heads/main/osmo-test-5/generated/frontend/assetlist.json + +interface AvailableNetworks { + chain: string; + networks: Array; +} + +export namespace OsmosisConfig { + export const tradingTypes = ['amm', 'clmm'] as const; // , 'router' previously referred to StableSwap but now deactivated (not many SS pools anyway) + export const networks = ['mainnet', 'testnet'] as const; + export const chainNames = ['osmosis-1', 'osmo-test-5'] as const; + export const chain = 'cosmos'; + + export interface NetworkConfig { + chainType: string; + chainName: (network: string) => string; + nodeURL: (network: string) => string; + availableNetworks: Array; + tradingTypes: (type: string) => Array; + nativeCurrencySymbol: string; + feeTier: string; + gasAdjustment: number; + gasLimitTransaction: number; + manualGasPrice: number; + manualGasPriceToken: string; + allowedSlippage: string; + rpcAddressDynamicBaseFee: string; + useEIP1559DynamicBaseFeeInsteadOfManualGasPrice: boolean; + defaultNetwork: string; + defaultWallet: string; + } + + export const config: NetworkConfig = { + chainType: 'cosmos', + chainName: (network: string) => ConfigManagerV2.getInstance().get(`osmosis.networks.${network}.chainName`), + nodeURL: (network: string) => ConfigManagerV2.getInstance().get(`osmosis.networks.${network}.nodeURL`), + availableNetworks: [ + { + chain: 'osmosis', + networks: ['mainnet', 'testnet'], + }, + ], + tradingTypes: (type: string) => { + return type === 'router' ? ['AMM'] : ['CLMM']; + }, + manualGasPrice: ConfigManagerV2.getInstance().get('osmosis.manualGasPrice'), + manualGasPriceToken: ConfigManagerV2.getInstance().get('osmosis.manualGasPriceToken'), + nativeCurrencySymbol: ConfigManagerV2.getInstance().get('osmosis.nativeCurrencySymbol'), + gasLimitTransaction: ConfigManagerV2.getInstance().get('osmosis.gasLimitTransaction'), + gasAdjustment: ConfigManagerV2.getInstance().get('osmosis.gasAdjustment'), + allowedSlippage: ConfigManagerV2.getInstance().get('osmosis.allowedSlippage'), + feeTier: ConfigManagerV2.getInstance().get('osmosis.feeTier'), + useEIP1559DynamicBaseFeeInsteadOfManualGasPrice: ConfigManagerV2.getInstance().get( + 'osmosis.useEIP1559DynamicBaseFeeInsteadOfManualGasPrice', + ), + rpcAddressDynamicBaseFee: ConfigManagerV2.getInstance().get('osmosis.rpcAddressDynamicBaseFee'), + defaultNetwork: ConfigManagerV2.getInstance().get('osmosis.defaultNetwork'), + defaultWallet: ConfigManagerV2.getInstance().get('osmosis.defaultWallet'), + }; +} diff --git a/src/connectors/osmosis/osmosis.controllers.ts b/src/connectors/osmosis/osmosis.controllers.ts new file mode 100755 index 0000000000..606fb17068 --- /dev/null +++ b/src/connectors/osmosis/osmosis.controllers.ts @@ -0,0 +1,823 @@ +import { decodeTxRaw } from '@cosmjs/proto-signing'; +import { BigNumber } from 'bignumber.js'; +import { Decimal } from 'decimal.js-light'; +import { FastifyInstance } from 'fastify'; + +import { CosmosWallet } from '../../chains/cosmos/cosmos-base'; +import { toCosmosBalances } from '../../chains/cosmos/cosmos.controllers'; +import { CosmosBalanceRequest } from '../../chains/cosmos/cosmos.requests'; +import { CosmosAsset } from '../../chains/cosmos/cosmos.universaltypes'; +import { + PoolInfo as AMMPoolInfo, + GetPoolInfoRequestType as AMMGetPoolInfoRequestType, + PositionInfo as AMMPositionInfo, + GetPositionInfoRequestType as AMMGetPositionInfoRequestType, + AddLiquidityRequestType as AMMAddLiquidityRequestType, + AddLiquidityResponseType as AMMAddLiquidityResponseType, + RemoveLiquidityRequestType as AMMRemoveLiquidityRequestType, + RemoveLiquidityResponseType as AMMRemoveLiquidityResponseType, +} from '../../schemas/amm-schema'; +import { + TokensRequestType, + TokensResponseType, + EstimateGasResponse, + PollResponseType, + PollRequestType, +} from '../../schemas/chain-schema'; +import { + CollectFeesRequestType as CLMMCollectFeesRequestType, + CollectFeesResponseType as CLMMCollectFeesResponseType, + OpenPositionRequestType as CLMMOpenPositionRequestType, + OpenPositionResponseType as CLMMOpenPositionResponseType, + ClosePositionRequestType as CLMMClosePositionRequestType, + ClosePositionResponseType as CLMMClosePositionResponseType, + PoolInfo as CLMMPoolInfo, + GetPoolInfoRequestType as CLMMGetPoolInfoRequestType, + PositionInfo as CLMMPositionInfo, + GetPositionInfoRequestType as CLMMGetPositionInfoRequestType, + AddLiquidityRequestType as CLMMAddLiquidityRequestType, + AddLiquidityResponseType as CLMMAddLiquidityResponseType, + RemoveLiquidityRequestType as CLMMRemoveLiquidityRequestType, + RemoveLiquidityResponseType as CLMMRemoveLiquidityResponseType, + FetchPoolsRequestType, + QuoteSwapResponseType, + QuoteSwapRequestType, + ExecuteSwapRequestType, + ExecuteSwapResponseType, +} from '../../schemas/clmm-schema'; +import { logger } from '../../services/logger'; + +import { Osmosis } from './osmosis'; +import { + PriceAndSerializableExtendedPools, + TransactionResponse, + TransferRequest, + TransferResponse, + AnyTransactionResponse, + OsmosisExpectedTrade, + TradeInfo, + TransactionEvent, + TransactionEventAttribute, + SerializableExtendedPool, +} from './osmosis.types'; + +// Osmosis transaction.code values +const successfulTransaction = 0; +const unconfirmedTransaction = 0; +const lessThanMinAmountSlippage = 7; +const insufficientFunds = 5; +const outOfGas = 11; + +export async function getOsmoWallet( + osmosis: Osmosis, + address: string, +): Promise<{ + wallet: CosmosWallet; +}> { + let wallet: CosmosWallet; + wallet = undefined; + try { + wallet = await osmosis.getWallet(address, 'osmo'); + } catch (err) { + logger.error(`Osmosis: Wallet ${address} not available.`); + logger.error(err); + } + return { wallet }; +} + +export class OsmosisController { + static async getTradeInfo( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: QuoteSwapRequestType, + poolType: string, + ): Promise<[TradeInfo, QuoteSwapResponseType]> { + try { + const gasAdjustment = osmosis.gasAdjustment; // + const feeTier = osmosis.feeTier; // + const baseAssetSymbol = req.baseToken; + const quoteAssetSymbol = req.quoteToken; + const baseAmount = new Decimal(req.amount); + const tradeSide = req.side; + + const allowedSlippage = req.slippagePct ? req.slippagePct : osmosis.getAllowedSlippage(); + + const baseToken: CosmosAsset = osmosis.getTokenBySymbol(baseAssetSymbol)!; + const quoteToken: CosmosAsset = osmosis.getTokenBySymbol(quoteAssetSymbol)!; + + if (!baseToken || !quoteToken) { + throw _fastify.httpErrors.notFound(`Token not found: ${!baseToken ? req.baseToken : req.quoteToken}`); + } + logger.info(`Base token (${req.baseToken}) info: ${JSON.stringify(baseToken)}`); + logger.info(`Quote token (${req.quoteToken}) info: ${JSON.stringify(quoteToken)}`); + + const requestAmount: BigNumber = BigNumber(baseAmount.toFixed(baseToken.decimals)); + + const expectedTrade: OsmosisExpectedTrade = await osmosis.estimateTrade( + osmosis.network, + quoteToken, + baseToken, + requestAmount, + tradeSide, + poolType, + req.poolAddress, + allowedSlippage, + feeTier, + gasAdjustment, + ); + const tradeInfo: TradeInfo = { + baseToken: baseToken, + quoteToken: quoteToken, + expectedTrade: expectedTrade, + requestAmount: requestAmount, + }; + + let finalPoolAddress = req.poolAddress; + if ( + tradeInfo && + tradeInfo.expectedTrade && + tradeInfo.expectedTrade.routes && + tradeInfo.expectedTrade.routes.length > 1 + ) { + // finalPoolAddress = 'multiple pool hops'; + finalPoolAddress = tradeInfo.expectedTrade.routes[0].poolId; + } else if ( + tradeInfo && + tradeInfo.expectedTrade && + tradeInfo.expectedTrade.routes && + tradeInfo.expectedTrade.routes.length == 1 + ) { + finalPoolAddress = tradeInfo.expectedTrade.routes[0].poolId; + } + + return [ + tradeInfo, + { + priceImpactPct: expectedTrade.priceImpact, + slippagePct: allowedSlippage, + amountIn: Number(expectedTrade.tokenInAmount), + amountOut: Number(expectedTrade.tokenOutAmount), + tokenIn: req.baseToken, + tokenOut: req.quoteToken, + price: tradeInfo.expectedTrade.executionPrice.toNumber(), + poolAddress: finalPoolAddress, + minAmountOut: Number(expectedTrade.tokenOutAmountAfterSlippage), + maxAmountIn: Number(expectedTrade.tokenInAmountAfterSlippage), + }, + ]; + } catch (e) { + throw _fastify.httpErrors.internalServerError(e); + } + } + + static async quoteSwap( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: QuoteSwapRequestType, + poolType: string, + ): Promise { + let swapQuoteResponse: QuoteSwapResponseType; + try { + const tradeInfoAndSwapQuote = await this.getTradeInfo(osmosis, _fastify, req, poolType); + swapQuoteResponse = tradeInfoAndSwapQuote[1]; + } catch (e) { + if (e instanceof Error) { + logger.error(`Osmosis: Could not get trade info. ${e.message} ${e.stack} ${e.stack}`); + throw _fastify.httpErrors.internalServerError( + `Osmosis: Could not get trade info. ${e.message} ${e.stack} ${e.stack}`, + ); + } else { + logger.error(`Osmosis: Could not get trade info. Reason Unknown`); + throw _fastify.httpErrors.internalServerError(`Osmosis: Could not get trade info. Reason Unknown`); + } + } + return swapQuoteResponse; + } + + static async executeSwap( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: ExecuteSwapRequestType, + poolType: string, + ): Promise { + try { + // const limitPrice = req.limitPrice; // MISSING? + const { wallet } = await getOsmoWallet(osmosis, req.walletAddress); + + let tradeInfo: TradeInfo; + try { + const tradeInfoAndSwapQuote = await this.getTradeInfo(osmosis, _fastify, req, poolType); + tradeInfo = tradeInfoAndSwapQuote[0]; + } catch (e) { + if (e instanceof Error) { + logger.error(`Osmosis: Could not get trade info. ${e.message} ${e.stack}`); + throw _fastify.httpErrors.internalServerError( + `Osmosis: Could not get trade info. ${req.baseToken} : ${req.quoteToken} - ${e.message} ${e.stack}`, + ); + } else { + logger.error(`Osmosis: Could not get trade info. Reason Unknown`); + throw _fastify.httpErrors.internalServerError( + `Osmosis: Could not get trade info. ${req.baseToken} : ${req.quoteToken} - Reason Unknown`, + ); + } + } + const gasAdjustment = osmosis.gasAdjustment; + const feeTier = osmosis.feeTier; + + // LOGIC FOR LIMIT_PRICE - do not remove unless there's something I'm missing... + // const price = tradeInfo.expectedTrade.executionPrice; + // if (req.side === 'BUY') { + // if ( + // limitPrice && + // new Decimal(price.toFixed(8)).gt(new Decimal(limitPrice)) + // ) { + // logger.error('Osmosis: Trade failed. Swap price exceeded limit price: ' + price.toFixed(8) + ' > ' + limitPrice); + // throw _fastify.httpErrors.badRequest('Osmosis: Trade failed. Swap price exceeded limit price: ' + price.toFixed(8) + ' > ' + limitPrice); + // } + // } + // else { + // logger.info( + // `Osmosis: Expected execution price is ${price.toFixed(6)}, ` + + // `limit price is ${limitPrice}.` + // ); + // if ( + // limitPrice && + // new Decimal(price.toFixed(8)).lt(new Decimal(limitPrice)) + // ) { + // logger.error('Osmosis: Trade failed. Swap price lower than limit price: ' + price.toFixed(8) + ' < ' + limitPrice); + // throw _fastify.httpErrors.badRequest('Osmosis: Trade failed. Swap price lower than limit price: ' + price.toFixed(8) + ' < ' + limitPrice); + // } + // } + + let balance_start_baseToken = new BigNumber(0); + let balance_start_quoteToken = new BigNumber(0); + let balance_end_baseToken = new BigNumber(0); + let balance_end_quoteToken = new BigNumber(0); + let transactionResponse: TransactionResponse; + + try { + const start_balances = await osmosis.getBalances(wallet); + balance_start_baseToken = start_balances[req.baseToken].value; + balance_start_quoteToken = start_balances[req.quoteToken].value; + transactionResponse = await osmosis.executeTrade( + osmosis.network, + wallet, + req, + tradeInfo, + feeTier, + gasAdjustment, + ); + const end_balances = await osmosis.getBalances(wallet); + balance_end_baseToken = end_balances[req.baseToken].value; + balance_end_quoteToken = end_balances[req.quoteToken].value; + } catch (e) { + if (e instanceof Error) { + logger.error(`Osmosis: Could not get trade info. ${e.message} ${e.stack}`); + throw _fastify.httpErrors.badRequest( + `Osmosis: Could not get trade info. ${req.baseToken} : ${req.quoteToken} - ${e.message} ${e.stack}`, + ); + } else { + logger.error(`Osmosis: Could not get trade info. Reason Unknown`); + throw _fastify.httpErrors.badRequest( + `Osmosis: Could not get trade info. ${req.baseToken} : ${req.quoteToken} - Reason Unknown`, + ); + } + } + + const tx = transactionResponse; + const txMessage = 'Trade has been executed. '; + this.validateTxErrors(tx, txMessage); + + let finalAmountReceived_string = ''; + for (let txEvent_idx = 0; txEvent_idx < tx.events.length; txEvent_idx++) { + const txEvent: TransactionEvent = tx.events[txEvent_idx]; + if (txEvent.type == 'coin_received') { + for (let txEventAttribute_idx = 0; txEventAttribute_idx < txEvent.attributes.length; txEventAttribute_idx++) { + const txEventAttribute: TransactionEventAttribute = txEvent.attributes[txEventAttribute_idx]; + if (txEventAttribute.key == 'receiver') { + if (txEventAttribute.value == req.walletAddress) { + const next_txEventAttribute: TransactionEventAttribute = txEvent.attributes[txEventAttribute_idx + 1]; + if (next_txEventAttribute.key == 'amount' && next_txEventAttribute.value) { + finalAmountReceived_string = next_txEventAttribute.value; + } + } + } + } + } + } + let finalAmountReceived = new BigNumber(0); + if (finalAmountReceived_string != '') { + finalAmountReceived = new BigNumber(finalAmountReceived_string.replace(tradeInfo.quoteToken.base, '')) + .shiftedBy(tradeInfo.quoteToken.decimals * -1) + .decimalPlaces(tradeInfo.quoteToken.decimals); + } + + const totalOutputSwapped = new BigNumber(req.amount) + .shiftedBy(tradeInfo.baseToken.decimals) + .decimalPlaces(tradeInfo.baseToken.decimals); + + const balanceChange_baseToken = balance_end_baseToken.minus(balance_start_baseToken).toNumber(); + const balanceChange_quoteToken = balance_end_quoteToken.minus(balance_start_quoteToken).toNumber(); + const executeSwapResponse: ExecuteSwapResponseType = { + data: { + tokenIn: req.baseToken, + tokenOut: req.quoteToken, + amountIn: finalAmountReceived.toNumber(), + amountOut: totalOutputSwapped.toNumber(), + fee: Number(tx.feeAmount), + baseTokenBalanceChange: balanceChange_baseToken, + quoteTokenBalanceChange: balanceChange_quoteToken, + }, + signature: tx.transactionHash, + status: 0, + }; + return executeSwapResponse; + } catch (e) { + throw _fastify.httpErrors.internalServerError(e); + } + } + + static async addLiquidityAMM( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: AMMAddLiquidityRequestType, + ): Promise { + const signature: string = ''; + let addLiquidityResponse: AMMAddLiquidityResponseType = { + signature, + status: 1, + }; + const { wallet } = await getOsmoWallet(osmosis, req.walletAddress); + const gasAdjustment = osmosis.gasAdjustment; // + const feeTier = osmosis.feeTier; // + try { + const txAndAddPositionResponse = await osmosis.addLiquidityAMM(wallet, req, feeTier, gasAdjustment); + const tx = txAndAddPositionResponse[0]; + addLiquidityResponse = txAndAddPositionResponse[1]; + this.validateTxErrors(tx, 'Liquidity added. '); + return addLiquidityResponse; + } catch (err) { + console.error('Osmosis: ' + err.message); + throw _fastify.httpErrors.internalServerError(err); + } + + return addLiquidityResponse; + } + + // Required before AddLiquityCLMM + static async openPositionCLMM( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: CLMMOpenPositionRequestType, + ): Promise { + try { + const { wallet } = await getOsmoWallet(osmosis, req.walletAddress); + const txAndOpenPositionResponse = await osmosis.OpenPositionCLMM(wallet, req as CLMMOpenPositionRequestType); + const openPositionTx = txAndOpenPositionResponse[0]; + const openPositionResponse: CLMMOpenPositionResponseType = txAndOpenPositionResponse[1]; + this.validateTxErrors(openPositionTx, 'CLMM Position Opened.'); + return openPositionResponse; + } catch (err) { + throw _fastify.httpErrors.internalServerError(err); + } + } + + // Apparently CLMMAddLiquidityRequestType doesn't contain baseToken/quoteToken, meaning it requires an OpenPositionRequest first... + static async addLiquidityCLMM( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: CLMMAddLiquidityRequestType, + ): Promise { + try { + const { wallet } = await getOsmoWallet(osmosis, req.walletAddress); + const txAndAddLiquidityResponse = await osmosis.AddLiquidityCLMM(wallet, req as CLMMAddLiquidityRequestType); + const tx = txAndAddLiquidityResponse[0]; + const addLiquidityResponse = txAndAddLiquidityResponse[1]; + this.validateTxErrors(tx, 'Liquidity added. '); + return addLiquidityResponse; + } catch (err) { + throw _fastify.httpErrors.internalServerError(err); + } + } + + static async removeLiquidityAMM( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: AMMRemoveLiquidityRequestType, + ): Promise { + try { + const { wallet } = await getOsmoWallet(osmosis, req.walletAddress); + const gasAdjustment = osmosis.gasAdjustment; // + const feeTier = osmosis.feeTier; // + const txAndReduceResponse = await osmosis.removeLiquidityAMM(wallet, req, feeTier, gasAdjustment); + const tx = txAndReduceResponse[0]; + const reduceLiquidityResponse = txAndReduceResponse[1]; + + logger.info(`Osmosis: Liquidity removed, txHash is ${tx.transactionHash}, gasUsed is ${tx.gasUsed}.`); + return reduceLiquidityResponse; + } catch (err) { + throw _fastify.httpErrors.internalServerError(err); + } + } + + static async removeLiquidityCLMM( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: CLMMRemoveLiquidityRequestType, + ): Promise { + try { + const { wallet } = await getOsmoWallet(osmosis, req.walletAddress); + const response = await osmosis.removeLiquidityCLMM(wallet, req); + return response; + } catch (err) { + throw _fastify.httpErrors.internalServerError(err); + } + } + + // this is the same as removeLiquidityCLMM but with collectFees first + static async closePositionCLMM( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: CLMMClosePositionRequestType, + ): Promise { + try { + const { wallet } = await getOsmoWallet(osmosis, req.walletAddress); + const responseCollect = await osmosis.collectRewardsIncentives(wallet, req); + const req_remove: CLMMRemoveLiquidityRequestType = { + walletAddress: req.walletAddress, + percentageToRemove: 100, + positionAddress: req.positionAddress, + }; + + const responseRemove: CLMMRemoveLiquidityResponseType = await osmosis.removeLiquidityCLMM(wallet, req_remove); + + let final_fee = 0; + if (responseRemove.data.fee) { + final_fee += responseRemove.data.fee; + } + if (responseCollect.data.fee) { + final_fee += responseCollect.data.fee; + } + const finalResponse: CLMMClosePositionResponseType = { + signature: responseRemove.signature, + status: responseRemove.status, + data: { + fee: final_fee, + baseTokenAmountRemoved: responseRemove.data.baseTokenAmountRemoved, + quoteTokenAmountRemoved: responseRemove.data.quoteTokenAmountRemoved, + baseFeeAmountCollected: responseCollect.data.baseFeeAmountCollected, + quoteFeeAmountCollected: responseCollect.data.quoteFeeAmountCollected, + positionRentRefunded: 0, + }, + }; + + return finalResponse; + } catch (err) { + throw _fastify.httpErrors.internalServerError(err); + } + } + + static async collectFees( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: CLMMCollectFeesRequestType, + ): Promise { + try { + const { wallet } = await getOsmoWallet(osmosis, req.walletAddress); + const response = await osmosis.collectRewardsIncentives(wallet, req); + return response; + } catch (err) { + throw _fastify.httpErrors.internalServerError(err); + } + } + + // find pool by poolAddress, or both token0, token1 (support for latter option now gone in HBOT main) + // we return multiple pools if we find for token pair but take one with highest liquidity - support lacking from hbot main + static async poolInfoRequest( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: AMMGetPoolInfoRequestType | CLMMGetPoolInfoRequestType, // these types are actually identical... but keeping || in case of future changes + poolType: string, + ): Promise { + let token0: CosmosAsset; + let token1: CosmosAsset; + + const priceAndPools: PriceAndSerializableExtendedPools = await osmosis.findPoolsPrices( + poolType, + req.poolAddress, + token0, + token1, + ); + + if (!priceAndPools.pools || priceAndPools.pools.length == 0) { + logger.error( + `Osmosis: Pool info request failed - no pools found for poolAddress, baseToken, quoteToken: ${req.poolAddress}`, //, ${req.baseToken}, ${req.quoteToken}.`, + ); + throw _fastify.httpErrors.notFound( + `Osmosis: Pool info request failed - no pools found for poolAddress, baseToken, quoteToken: ${req.poolAddress}`, //, ${req.baseToken}, ${req.quoteToken}.`, + ); + } + if (!priceAndPools.prices || priceAndPools.prices.length == 0) { + logger.error( + `Osmosis: Pool info request failed - no pools found for poolAddress, baseToken, quoteToken: ${req.poolAddress}`, //, ${req.baseToken}, ${req.quoteToken}.`, + ); + throw _fastify.httpErrors.notFound( + `Osmosis: Pool info request failed - no pools found for poolAddress, baseToken, quoteToken: ${req.poolAddress}`, //, ${req.baseToken}, ${req.quoteToken}.`, + ); + } + + logger.info( + `Osmosis: Pool Info Request completed for poolAddress, baseToken, quoteToken: ${req.poolAddress}`, //, ${req.baseToken}, ${req.quoteToken}.`, + ); + + const pool = priceAndPools.pools[0]; + const price = priceAndPools.prices[0]; + + if (poolType == 'amm') { + const poolResponseAMM: AMMPoolInfo = { + address: pool.address, + baseTokenAddress: pool.poolAssets[0].token.denom, // base token denom - sort of works as an address in this case + quoteTokenAddress: pool.poolAssets[1].token.denom, + feePct: Number(pool.swapFee), + price: Number(price), + baseTokenAmount: Number(pool.poolAssets[0].token.amount), + quoteTokenAmount: Number(pool.poolAssets[1].token.amount), + }; + return poolResponseAMM; + } else { + //poolType == "clmm" + const poolResponseCLMM: CLMMPoolInfo = { + address: pool.address, + baseTokenAddress: pool.token0, + quoteTokenAddress: pool.token1, + feePct: Number(pool.swapFee), + price: Number(price), + baseTokenAmount: Number(pool.currentTickLiquidity), + quoteTokenAmount: Number(pool.currentTickLiquidity), + binStep: Number(pool.tickSpacing), + activeBinId: Number(pool.currentTick), + }; + return poolResponseCLMM; + } + } + + static async fetchPoolsForTokens( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: FetchPoolsRequestType, + poolType: string, + ): Promise { + const token0: CosmosAsset = osmosis.getTokenBySymbol(req.tokenA)!; + const token1: CosmosAsset = osmosis.getTokenBySymbol(req.tokenB)!; + if (!token0) { + logger.error('Osmosis: baseToken not supported: ' + req.tokenA); + throw _fastify.httpErrors.badRequest('Osmosis: baseToken not supported: ' + req.tokenA); + } + if (!token1) { + logger.error('Osmosis: quoteToken not supported: ' + req.tokenB); + throw _fastify.httpErrors.badRequest('Osmosis: quoteToken not supported: ' + req.tokenB); + } + + const priceAndPools: PriceAndSerializableExtendedPools = await osmosis.findPoolsPrices( + poolType, + '', + token0, + token1, + ); + + if (!priceAndPools.pools || priceAndPools.pools.length == 0) { + logger.error( + `Osmosis: Fetch pools for tokens request failed - no pools found for baseToken, quoteToken: ${req.tokenA}, ${req.tokenB}.`, + ); + throw _fastify.httpErrors.notFound( + `Osmosis: Fetch pools for tokens request failed - no pools found for baseToken, quoteToken: ${req.tokenA}, ${req.tokenB}.`, + ); + } + if (!priceAndPools.prices || priceAndPools.prices.length == 0) { + logger.error( + `Osmosis: Fetch pools for tokens request failed - no pools found for baseToken, quoteToken: ${req.tokenA}, ${req.tokenB}.`, + ); + throw _fastify.httpErrors.notFound( + `Osmosis: Fetch pools for tokens request failed - no pools found for baseToken, quoteToken: ${req.tokenA}, ${req.tokenB}.`, + ); + } + + logger.info( + `Osmosis: Fetch pools for tokens request completed - no pools found for baseToken, quoteToken: ${req.tokenA}, ${req.tokenB}.`, + ); + + return priceAndPools.pools; + } + + // find singular positionId + static async poolPosition( + osmosis: Osmosis, + _fastify: FastifyInstance, + req: AMMGetPositionInfoRequestType | CLMMGetPositionInfoRequestType, + poolType: string, + ): Promise { + try { + let response; + if (poolType == 'amm') { + response = (await osmosis.findPoolsPositionsGAMM(req as AMMGetPositionInfoRequestType)) as AMMPositionInfo[]; + } else { + response = (await osmosis.findPoolsPositionsCLMM(req as CLMMGetPositionInfoRequestType)) as CLMMPositionInfo[]; + } + + return { + network: osmosis.network, + ...response[0], + }; + } catch (err) { + throw _fastify.httpErrors.internalServerError(err); + } + } + + // find all pool positions for address or singular poolId if supplied + static async allPoolPositions( + osmosis: Osmosis, + _fastify: FastifyInstance, + walletAddress: string, + poolType: string, + ): Promise { + try { + let response; + if (poolType == 'amm') { + response = (await osmosis.findPoolsPositionsGAMM({ + walletAddress: walletAddress, + } as AMMGetPositionInfoRequestType)) as AMMPositionInfo[]; + } else { + response = (await osmosis.findPoolsPositionsCLMM( + { + walletAddress: walletAddress, + positionAddress: 'NONE', + } as CLMMGetPositionInfoRequestType, + true, + )) as CLMMPositionInfo[]; + } + + return { + network: osmosis.network, + ...response, + }; + } catch (err) { + throw _fastify.httpErrors.internalServerError(err); + } + } + + static async getTokens(osmosis: Osmosis, req: TokensRequestType): Promise { + return await osmosis.getTokens(req); + } + + static async transfer(osmosis: Osmosis, _fastify: FastifyInstance, req: TransferRequest): Promise { + const { wallet } = await getOsmoWallet(osmosis, req.from); + + const token: CosmosAsset = osmosis.getTokenBySymbol(req.token)!; + const tx = await osmosis.transfer(wallet, token, req); + + const txMessage = 'Transfer success. To: ' + req.to + ' From: ' + req.from + ' '; + this.validateTxErrors(tx, txMessage); + if (tx.code == successfulTransaction) { + return ( + 'Transfer success. To: ' + + req.to + + ' From: ' + + req.from + + ' Hash: ' + + tx.transactionHash + + ' gasUsed: ' + + tx.gasUsed + + ' gasWanted: ' + + tx.gasWanted + ); + } else { + logger.error('Osmosis: Transfer failed. ' + tx.rawLog); + throw _fastify.httpErrors.badRequest('Osmosis: Transfer failed. ' + tx.rawLog); + } + } + + static async estimateGas(osmosis: Osmosis, _fastify: FastifyInstance): Promise { + try { + const gasPrice = await osmosis.getLatestBasePrice(); + const gasLimitUsed = osmosis.gasLimitTransaction; + return { + timestamp: Date.now(), + denomination: osmosis.nativeTokenSymbol, + feePerComputeUnit: gasLimitUsed, + computeUnits: 0, + feeAsset: 'OSMO', + fee: gasPrice, + }; + } catch (err) { + throw _fastify.httpErrors.internalServerError(err); + } + } + + static async balances(osmosis: Osmosis, _fastify: FastifyInstance, req: CosmosBalanceRequest) { + try { + const wallet = await osmosis.getWallet(req.address, 'osmo'); + let replyWithAllTokens = false; + let tokenSymbols: string[] = []; + // FILTER IF req.tokenSymbols != [] + if (req && req.tokenSymbols && req.tokenSymbols.length > 0) { + req.tokenSymbols.forEach((sym) => { + const tsym = osmosis.getToken(sym); + if (!tsym || tsym == undefined) { + throw _fastify.httpErrors.notFound('Invalid token symbol: ' + sym); + } + }); + tokenSymbols = req.tokenSymbols; + } else { + replyWithAllTokens = true; + } + + const balances = await osmosis.getBalances(wallet); + const filteredBalances = toCosmosBalances(balances, osmosis.storedTokenList, replyWithAllTokens); + + return { + network: osmosis.network, + balances: filteredBalances, + wallet: req.address, + }; + } catch (err) { + throw _fastify.httpErrors.internalServerError(err); + } + } + + static async poll(osmosis: Osmosis, request: PollRequestType): Promise { + try { + const response = await osmosis.getTransaction(request.signature); + const currentBlock = await osmosis.getCurrentBlockNumber(); + let tokenBalanceChanges: Record = {}; + { + const dissectRes = (await osmosis.dissectTransactionResponse(request.walletAddress, undefined, response)) as [ + Record, + Record, + string, + ]; + tokenBalanceChanges = dissectRes[1]; + } + + let fee = 0; + if ( + response.tx.authInfo && + response.tx.authInfo.fee && + response.tx.authInfo.fee.amount && + response.tx.authInfo.fee.amount[0] + ) { + fee = Number(response.tx.authInfo.fee.amount[0].amount.toString()); + } + + const response_out: PollResponseType = { + tokenBalanceChanges: tokenBalanceChanges, + currentBlock: currentBlock, + signature: request.signature, + txStatus: response.txResponse.code, + txBlock: Number(response.txResponse.height.toString()), + fee: fee, + txData: response.txResponse.events, + }; + return response_out; + } catch (error) { + if (error.statusCode) { + throw error; // Re-throw if it's already a Fastify error + } + logger.error(`Error polling transaction: ${error.message}`); + } + } + + static validateTxErrors(tx: AnyTransactionResponse, msg: string) { + if (tx.code != successfulTransaction) { + if (tx.code == outOfGas) { + logger.error( + `Osmosis: Failed to execute trade: Out of gas. txHash is ${tx.transactionHash}, gasUsed is ${tx.gasUsed} greater than gasWanted ${tx.gasWanted} . Log: ${tx.rawLog}`, + ); + throw new Error( + `Osmosis: Failed to execute trade: Out of gas. txHash is ${tx.transactionHash}, gasUsed is ${tx.gasUsed} greater than gasWanted ${tx.gasWanted} . Log: ${tx.rawLog}`, + ); + } else if (tx.code == lessThanMinAmountSlippage) { + logger.error( + `Osmosis: Failed to execute trade. Token created less than minimum amount; increase slippage. txHash is ${tx.transactionHash}, gasUsed is ${tx.gasUsed}. Log: ${tx.rawLog}`, + ); + throw new Error( + `Osmosis: Failed to execute trade. Token created less than minimum amount; increase slippage. txHash is ${tx.transactionHash}, gasUsed is ${tx.gasUsed}. Log: ${tx.rawLog}`, + ); + } else if (tx.code == insufficientFunds) { + logger.error( + `Osmosis: Failed to execute trade. Insufficient funds. txHash is ${tx.transactionHash}, gasUsed is ${tx.gasUsed}. Log: ${tx.rawLog}`, + ); + throw new Error( + `Osmosis: Failed to execute trade. Insufficient funds. txHash is ${tx.transactionHash}, gasUsed is ${tx.gasUsed}. Log: ${tx.rawLog}`, + ); + } else { + logger.error( + `Osmosis: Failed to execute trade. txHash is ${tx.transactionHash}, gasUsed is ${tx.gasUsed}. Log: ${tx.rawLog}`, + ); + throw new Error( + `Osmosis: Failed to execute trade. txHash is ${tx.transactionHash}, gasUsed is ${tx.gasUsed}. Log: ${tx.rawLog}`, + ); + } + } + logger.info('Osmosis: ' + msg + `txHash is ${tx.transactionHash}, gasUsed is ${tx.gasUsed}.`); + } +} diff --git a/src/connectors/osmosis/osmosis.pools.utils.ts b/src/connectors/osmosis/osmosis.pools.utils.ts new file mode 100755 index 0000000000..87831f3ba5 --- /dev/null +++ b/src/connectors/osmosis/osmosis.pools.utils.ts @@ -0,0 +1,377 @@ +// adapted from utils/pools.ts from createcosmoapp liquidity example +import { Asset } from '@chain-registry/types'; +import { Coin } from '@cosmjs/amino'; +import { + calcPoolLiquidity, + getPoolByGammName as _getPoolByGammName, + convertGammTokenToDollarValue, +} from '@osmonauts/math'; +import { BigNumber } from 'bignumber.js'; +import { Pool } from 'osmojs/osmosis/gamm/v1beta1/balancerPool'; + +import { CosmosAsset, AssetList } from '../../chains/cosmos/cosmos.universaltypes'; + +import { PriceHash, ExtendedPool } from './osmosis.types'; +export const getPoolByGammName = (pools: Pool[], gammId: string): Pool => { + return _getPoolByGammName(pools, gammId); +}; + +//latest pool types: +// $typeUrl: "/osmosis.gamm.poolmodels.stableswap.v1beta1.Pool", +// $typeUrl: "/osmosis.gamm.v1beta1.Pool", +// $typeUrl: "/osmosis.concentratedliquidity.v1beta1.Pool", +// $typeUrl: "/osmosis.cosmwasmpool.v1beta1.CosmWasmPool", // these have .codeId and .poolId instead of .id - but not using + +// asset types are listed different by pool type: +const typeUrlAMM = 'osmosis.gamm.v1beta1.Pool'; // .poolAssets[].token.denom +const typeUrlCLMM = 'osmosis.concentratedliquidity.v1beta1.Pool'; // .token0 .token1 +const typeUrlStableSwap = 'osmosis.gamm.poolmodels.stableswap.v1beta1.Pool'; // .poolLiquidity[].denom + +export const getPoolByIdAndFilter = ( + assets: Asset[], + pools: any[], + prices: PriceHash, + findPoolId: bigint, + verifyAssetsAndPrices: boolean, +): ExtendedPool[] => { + let poolsOut = pools.filter(({ id }) => id == findPoolId); + if (verifyAssetsAndPrices) { + poolsOut = poolsOut.filter((pool) => { + if (pool.poolAssets) { + return pool.poolAssets.every( + (pAsset: { token: { denom: string } }) => + prices[pAsset.token.denom] && assets.find(({ base }) => base === pAsset.token.denom), + ); + } else if (pool.token0) { + if (pool.currentSqrtPrice && pool.currentSqrtPrice == '0') { + // filter out totally dead pools on testnet + return false; + } + return ( + prices[pool.token0] && + prices[pool.token1] && + assets.find(({ base }) => base === pool.token0) && + assets.find(({ base }) => base === pool.token1) + ); + } + return false; + }); + } + return poolsOut; +}; + +export const getPoolByAddressAndFilter = ( + assets: Asset[], + pools: any[], + prices: PriceHash, + poolAddress: string, + verifyAssetsAndPrices: boolean, +): ExtendedPool[] => { + let poolsOut = pools.filter(({ address }) => address == poolAddress); + if (verifyAssetsAndPrices) { + poolsOut = poolsOut.filter((pool) => { + if (pool.poolAssets) { + return pool.poolAssets.every( + (pAsset: { token: { denom: string } }) => + prices[pAsset.token.denom] && assets.find(({ base }) => base === pAsset.token.denom), + ); + } else if (pool.token0) { + if (pool.currentSqrtPrice && pool.currentSqrtPrice == '0') { + // filter out totally dead pools on testnet + return false; + } + return ( + prices[pool.token0] && + prices[pool.token1] && + assets.find(({ base }) => base === pool.token0) && + assets.find(({ base }) => base === pool.token1) + ); + } + return false; + }); + } + return poolsOut; +}; + +export const filterPoolsGAMM = (assets: Asset[], pools: any[], prices: PriceHash, verifyPrices: boolean = true) => { + let poolsOut = pools.filter(({ $typeUrl }) => $typeUrl?.includes(typeUrlAMM)); + + poolsOut = poolsOut.filter((pool) => { + if (pool.poolAssets) { + if (verifyPrices) { + return pool.poolAssets.every( + (pAsset: { token: { denom: string } }) => + prices[pAsset.token.denom] && assets.find(({ base }) => base === pAsset.token.denom), + ); + } else { + return pool.poolAssets.every((pAsset: { token: { denom: string } }) => + assets.find(({ base }) => base === pAsset.token.denom), + ); + } + } else if (pool.token0) { + if (verifyPrices) { + return ( + prices[pool.token0] && + prices[pool.token1] && + assets.find(({ base }) => base === pool.token0) && + assets.find(({ base }) => base === pool.token1) + ); + } else { + return assets.find(({ base }) => base === pool.token0) && assets.find(({ base }) => base === pool.token1); + } + } + return false; + }); + + return poolsOut; +}; + +export const filterPoolsStableSwap = (assets: Asset[], pools: any[], prices: PriceHash, verifyPrices: boolean) => { + let poolsOut = pools.filter(({ $typeUrl }) => $typeUrl?.includes(typeUrlStableSwap)); + + poolsOut = poolsOut.filter((pool) => { + if (pool.poolAssets) { + if (verifyPrices) { + return pool.poolAssets.every( + (pAsset: { token: { denom: string } }) => + prices[pAsset.token.denom] && assets.find(({ base }) => base === pAsset.token.denom), + ); + } else { + return pool.poolAssets.every((pAsset: { token: { denom: string } }) => + assets.find(({ base }) => base === pAsset.token.denom), + ); + } + } else if (pool.token0) { + if (verifyPrices) { + return ( + prices[pool.token0] && + prices[pool.token1] && + assets.find(({ base }) => base === pool.token0) && + assets.find(({ base }) => base === pool.token1) + ); + } else { + return assets.find(({ base }) => base === pool.token0) && assets.find(({ base }) => base === pool.token1); + } + } + return false; + }); + + return poolsOut; +}; + +export const filterPoolsCLMM = (assets: Asset[], pools: any[], prices: PriceHash, verifyPrices: boolean) => { + let poolsOut = pools.filter(({ $typeUrl }) => $typeUrl?.includes(typeUrlCLMM)); + + poolsOut = poolsOut.filter((pool) => { + if (pool.poolAssets) { + if (verifyPrices) { + return pool.poolAssets.every( + (pAsset: { token: { denom: string } }) => + prices[pAsset.token.denom] && assets.find(({ base }) => base === pAsset.token.denom), + ); + } else { + return pool.poolAssets.every((pAsset: { token: { denom: string } }) => + assets.find(({ base }) => base === pAsset.token.denom), + ); + } + } else if (pool.token0) { + if (pool.currentSqrtPrice && pool.currentSqrtPrice == '0') { + // filter out totally dead pools on testnet + return false; + } + if (verifyPrices) { + return ( + prices[pool.token0] && + prices[pool.token1] && + assets.find(({ base }) => base === pool.token0) && + assets.find(({ base }) => base === pool.token1) + ); + } else { + return assets.find(({ base }) => base === pool.token0) && assets.find(({ base }) => base === pool.token1); + } + } + return false; + }); + + return poolsOut; +}; + +interface ExtendPoolProps { + pool: ExtendedPool; + fees: Fee[]; + balances: Coin[]; + lockedCoins: Coin[]; + prices: PriceHash; +} + +export const extendPool = ( + assets: AssetList[], + { pool, fees, balances, lockedCoins, prices }: ExtendPoolProps, +): ExtendedPool => { + let liquidity = 0; + if (pool.poolAssets) { + // if this is NOT a CL pool + //@ts-expect-error: Osmosis Case 2 + liquidity = new BigNumber(calcPoolLiquidity(assets, pool, prices)).decimalPlaces(0).toNumber(); + } + + let volume24H = 0; + let volume7d = 0; + let fees7D = 0; + let swapFee = ''; + let exitFee = ''; + + if (pool.id) { + // removes cosmwasm pools + if (pool.poolParams && pool.poolParams.swapFee) { + swapFee = pool.poolParams.swapFee; // this might be a raw amount or need a *100, it's very unclear from docs. + } + if (pool.poolParams && pool.poolParams.exitFee) { + exitFee = pool.poolParams.exitFee; // this might be a raw amount or need a *100, it's very unclear from docs. + } + } + + if (fees) { + const feeData = fees.find((fee) => fee.pool_id === pool.id.toString()); + volume24H = Math.round(Number(feeData?.volume_24h || 0)); + volume7d = Math.round(Number(feeData?.volume_7d || 0)); + fees7D = Math.round(Number(feeData?.fees_spent_7d || 0)); + } + + let poolDenom = ''; + if (pool.totalShares) { + // also don't exist for CL pools + poolDenom = pool.totalShares?.denom; + } + + const balanceCoin = balances.find(({ denom }) => denom === poolDenom); + const myLiquidity = balanceCoin ? balanceCoin.amount : '0'; + const myLiquidityDollarValue = balanceCoin + ? //@ts-expect-error: Osmosis Case 2 + convertGammTokenToDollarValue(assets, balanceCoin, pool, prices) + : '0'; + + const lockedCoin = lockedCoins.find(({ denom }) => denom === poolDenom); + const bonded = lockedCoin + ? //@ts-expect-error: Osmosis Case 2 + convertGammTokenToDollarValue(assets, lockedCoin, pool, prices) + : '0'; + + return { + ...pool, + liquidity, + volume24H, + fees7D, + volume7d, + myLiquidity, + myLiquidityDollarValue, + bonded, + denom: poolDenom, + exitFee: exitFee, + swapFee: swapFee, + incentivesAddress: pool.incentivesAddress ? pool.incentivesAddress : '', + spreadRewardsAddress: pool.spreadRewardsAddress ? pool.spreadRewardsAddress : '', + }; +}; + +export const descByLiquidity = (pool1: ExtendedPool, pool2: ExtendedPool) => { + return new BigNumber(pool1.liquidity).lt(pool2.liquidity) ? 1 : -1; +}; + +export const descByMyLiquidity = (pool1: ExtendedPool, pool2: ExtendedPool) => { + return new BigNumber(pool1.myLiquidity).lt(pool2.myLiquidity) ? 1 : -1; +}; + +type Item = { + denom: string; + [key: string]: any; +}; + +export const getPoolsByDenom = (allPools: ExtendedPool[], items: Item[]) => { + return items + .filter(({ denom }) => allPools.find(({ denom: d }) => d === denom)) + .map(({ denom }) => { + return allPools.find(({ denom: d }) => d === denom) as ExtendedPool; + }); +}; + +export interface Fee { + pool_id: string; + volume_24h: number; + volume_7d: number; + // fees_spent_24h: number; + fees_spent_7d: number; + fees_percentage: string; + fee_swap?: string; + fee_exit?: string; +} + +// this api is deprecated - nowhere to get volume that i can find (for free) +export const fetchFees = async (): Promise => { + const url = 'https://api-osmosis.imperator.co/fees/v1/pools'; + return ( + fetch(url) + // .then(handleError) + .then((res) => res.json()) + .then((res) => res.data) + ); +}; + +// another API gone - parsing fees ourselves +export const parseFees = async (pools: ExtendedPool[]): Promise => { + const fees: Fee[] = []; + pools.forEach((epool) => { + if (epool.id) { + // removes cosmwasm pools + let fee_perc = '0'; + if (epool.poolParams && epool.poolParams.swapFee) { + fee_perc = epool.poolParams.swapFee; // this might be a raw amount or need a *100, it's very unclear from docs. + } + let fee_exit = '0'; + if (epool.poolParams && epool.poolParams.exitFee) { + fee_exit = epool.poolParams.exitFee; // this might be a raw amount or need a *100, it's very unclear from docs. + } + fees.push({ + pool_id: epool.id.toString(), + volume_24h: epool.volume24H, + volume_7d: epool.volume7d, + fees_spent_7d: epool.fees7D, + fee_exit: fee_exit, + fees_percentage: fee_perc, + fee_swap: fee_perc, + }); + } + }); + return fees; +}; + +export const CLMMMakePoolPairs = (tokenList, pools, liquidityLimit = 100_000) => { + let pools_out = pools.filter( + (pool) => + pool.token0 && + pool.token1 && + !pool.token0.startsWith('gamm') && + !pool.token1.startsWith('gamm') && + new BigNumber(pool.currentTickLiquidity).gte(liquidityLimit), + ); + + pools_out = pools_out.map((pool) => { + const assetA = pool.token0; + const assetAinfo = tokenList.find((token: CosmosAsset) => token.base === assetA); + const assetB = pool.token1; + const assetBinfo = tokenList.find((token: CosmosAsset) => token.base === assetB); + if (!assetAinfo || !assetBinfo) return; + return { + poolId: typeof pool.id === 'string' ? pool.id : pool.id.toString(), + poolAddress: pool.address, + baseName: assetAinfo.display, + baseSymbol: assetAinfo.symbol, + baseAddress: assetAinfo.base, + quoteName: assetBinfo.display, + quoteSymbol: assetBinfo.symbol, + quoteAddress: assetBinfo.base, + }; + }); + + return pools_out; +}; diff --git a/src/connectors/osmosis/osmosis.routes.ts b/src/connectors/osmosis/osmosis.routes.ts new file mode 100755 index 0000000000..681b55d447 --- /dev/null +++ b/src/connectors/osmosis/osmosis.routes.ts @@ -0,0 +1,61 @@ +import sensible from '@fastify/sensible'; +import { FastifyPluginAsync } from 'fastify'; + +// Import routes +import { osmosisAmmRoutes } from './amm-routes'; +import { osmosisClmmRoutes } from './clmm-routes'; +import { osmosisRouterRoutes } from './router-routes'; + +// Stableswap routes +const osmosisRouterRoutesWrapper: FastifyPluginAsync = async (fastify) => { + await fastify.register(sensible); + + await fastify.register(async (instance) => { + instance.addHook('onRoute', (routeOptions) => { + if (routeOptions.schema && routeOptions.schema.tags) { + routeOptions.schema.tags = ['/connector/osmosis']; + } + }); + + await instance.register(osmosisRouterRoutes); + }); +}; + +// AMM routes +const osmosisAmmRoutesWrapper: FastifyPluginAsync = async (fastify) => { + await fastify.register(sensible); + + await fastify.register(async (instance) => { + instance.addHook('onRoute', (routeOptions) => { + if (routeOptions.schema && routeOptions.schema.tags) { + routeOptions.schema.tags = ['/connector/osmosis']; + } + }); + + await instance.register(osmosisAmmRoutes); + }); +}; + +// CLMM routes +const osmosisClmmRoutesWrapper: FastifyPluginAsync = async (fastify) => { + await fastify.register(sensible); + + await fastify.register(async (instance) => { + instance.addHook('onRoute', (routeOptions) => { + if (routeOptions.schema && routeOptions.schema.tags) { + routeOptions.schema.tags = ['/connector/osmosis']; + } + }); + + await instance.register(osmosisClmmRoutes); + }); +}; + +// Export routes in the same pattern as other connectors +export const osmosisRoutes = { + router: osmosisRouterRoutesWrapper, + amm: osmosisAmmRoutesWrapper, + clmm: osmosisClmmRoutesWrapper, +}; + +export default osmosisRoutes; diff --git a/src/connectors/osmosis/osmosis.swap.ts b/src/connectors/osmosis/osmosis.swap.ts new file mode 100755 index 0000000000..2453b9747b --- /dev/null +++ b/src/connectors/osmosis/osmosis.swap.ts @@ -0,0 +1,253 @@ +import { CoinDenom, Trade, PrettyPair } from '@osmonauts/math/esm/types'; +import { symbolToOsmoDenom } from '@osmonauts/math/utils'; +import { BigNumber } from 'bignumber.js'; // adapted from osmonauts-math swap.ts to use BigNumber to enable .pow(x<1) +import { Decimal } from 'decimal.js'; +import { FastifyInstance } from 'fastify'; +import { Coin } from 'osmo-query/cosmos/base/v1beta1/coin'; +import { SwapAmountInRoute } from 'osmojs/osmosis/poolmanager/v1beta1/swap_route'; + +import { AssetList } from '../../chains/cosmos/cosmos.universaltypes'; +import { + QuoteSwapResponseType, + QuoteSwapRequestType, + ExecuteSwapRequestType, + ExecuteSwapResponseType, +} from '../../schemas/clmm-schema'; +import { logger } from '../../services/logger'; + +import { Osmosis } from './osmosis'; +import { ExtendedPool } from './osmosis.types'; + +export async function executeSwap( + fastify: FastifyInstance, + request: ExecuteSwapRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + const response: ExecuteSwapResponseType = await osmosis.controller.executeSwap(osmosis, fastify, request, poolType); + return response; +} + +export async function quoteSwap( + fastify: FastifyInstance, + request: QuoteSwapRequestType, + poolType: string, +): Promise { + let networkToUse = request.network ? request.network : 'mainnet'; + const osmosis = await Osmosis.getInstance(networkToUse); + await osmosis.init(); + networkToUse = osmosis.network; + logger.info(`Network: ${networkToUse}, Chain ID: ${osmosis.chainName}`); + + const response: QuoteSwapResponseType = await osmosis.controller.quoteSwap(osmosis, fastify, request, poolType); + return response; +} + +export const routesThroughPools = ({ + denom, + trade, + pairs, +}: { + denom: CoinDenom; + trade: Trade; + pairs: PrettyPair[]; +}): SwapAmountInRoute[] => { + const sellPool = pairs.find( + (pair) => + (pair.baseAddress == trade.sell.denom && pair.quoteAddress == denom) || + (pair.quoteAddress == trade.sell.denom && pair.baseAddress == denom), + ); + + const buyPool = pairs.find( + (pair) => + (pair.baseAddress == denom && pair.quoteAddress == trade.buy.denom) || + (pair.quoteAddress == denom && pair.baseAddress == trade.buy.denom), + ); + + if (sellPool && buyPool) { + const routes = [ + { + poolId: BigInt(sellPool.poolId), + tokenOutDenom: denom, + }, + { + poolId: BigInt(buyPool.poolId), + tokenOutDenom: trade.buy.denom, + }, + ]; + + return routes; + } + + return []; +}; + +export const getRoutesForTrade = ( + assets: AssetList[], + { + trade, + pairs, + }: { + trade: Trade; + pairs: PrettyPair[]; + }, +): SwapAmountInRoute[] => { + const directPool = pairs.find( + (pair) => + (pair.baseAddress == trade.sell.denom && pair.quoteAddress == trade.buy.denom) || + (pair.quoteAddress == trade.sell.denom && pair.baseAddress == trade.buy.denom), + ); + + if (directPool) { + return [ + { + poolId: BigInt(directPool.poolId), + tokenOutDenom: trade.buy.denom, + }, + ]; + } + + const osmoRoutes = routesThroughPools({ + denom: 'uosmo', + trade, + pairs, + }); + + if (osmoRoutes.length === 2) return osmoRoutes; + + const atomRoutes = routesThroughPools({ + // @ts-expect-error: Case 2 + denom: symbolToOsmoDenom(assets, 'ATOM'), + trade, + pairs, + }); + + if (atomRoutes.length === 2) return atomRoutes; + + return []; +}; + +export const calcAmountWithSlippage = (amount: string, slippage: number | string) => { + const remainingPercentage = new BigNumber(100).minus(slippage).div(100); + return new BigNumber(amount).multipliedBy(remainingPercentage).toString(); +}; + +const one = new BigNumber(1); + +const getPoolAsset = (pool: ExtendedPool, denom: string) => { + const poolAsset = pool.poolAssets.find((asset) => asset?.token && asset.token.denom === denom); + if (!poolAsset) { + throw new Error(`Pool ${pool.id} doesn't have the pool asset for ${denom}`); + } + return { denom, weight: poolAsset.weight, amount: poolAsset.token!.amount }; +}; + +export const calcSpotPrice = ( + tokenBalanceIn: BigNumber, + tokenWeightIn: BigNumber, + tokenBalanceOut: BigNumber, + tokenWeightOut: BigNumber, + swapFee: BigNumber, +): BigNumber => { + const number = tokenBalanceIn.div(tokenWeightIn); + const denom = tokenBalanceOut.div(tokenWeightOut); + const scale = one.div(one.minus(swapFee)); + + return number.div(denom).multipliedBy(scale); +}; + +export const calcOutGivenIn = ( + tokenBalanceIn: BigNumber, + tokenWeightIn: BigNumber, + tokenBalanceOut: BigNumber, + tokenWeightOut: BigNumber, + tokenAmountIn: BigNumber, + swapFee: BigNumber, +): BigNumber => { + const weightRatio = tokenWeightIn.div(tokenWeightOut); + let adjustedIn = one.minus(swapFee); + adjustedIn = tokenAmountIn.multipliedBy(adjustedIn); + const y = tokenBalanceIn.div(tokenBalanceIn.plus(adjustedIn)); + const foo = new BigNumber(new Decimal(y.toString()).pow(new Decimal(weightRatio.toString())).toString()); + const bar = one.minus(foo); + return tokenBalanceOut.multipliedBy(bar); +}; + +export const calcInGivenOut = ( + tokenBalanceIn: BigNumber, + tokenWeightIn: BigNumber, + tokenBalanceOut: BigNumber, + tokenWeightOut: BigNumber, + tokenAmountOut: BigNumber, + swapFee: BigNumber, +): BigNumber => { + const weightRatio = tokenWeightOut.div(tokenWeightIn); + const diff = tokenBalanceOut.minus(tokenAmountOut); + const y = tokenBalanceOut.div(diff); + let foo = new BigNumber(new Decimal(y.toString()).pow(new Decimal(weightRatio.toString())).toString()); + foo = foo.minus(one); + const tokenAmountIn = one.minus(swapFee); + return tokenBalanceIn.multipliedBy(foo).div(tokenAmountIn); +}; + +export const calcPriceImpactGivenIn = (tokenIn: Coin, tokenOutDenom: string, pool: ExtendedPool) => { + const inPoolAsset = getPoolAsset(pool, tokenIn.denom); + const outPoolAsset = getPoolAsset(pool, tokenOutDenom); + + const swapFee = new BigNumber(pool.poolParams?.swapFee || 0).shiftedBy(-18); + + const beforeSpotPriceInOverOut = calcSpotPrice( + new BigNumber(inPoolAsset.amount), + new BigNumber(inPoolAsset.weight), + new BigNumber(outPoolAsset.amount), + new BigNumber(outPoolAsset.weight), + swapFee, + ); + + const tokenOutAmount = calcOutGivenIn( + new BigNumber(inPoolAsset.amount), + new BigNumber(inPoolAsset.weight), + new BigNumber(outPoolAsset.amount), + new BigNumber(outPoolAsset.weight), + new BigNumber(tokenIn.amount), + swapFee, + ).decimalPlaces(0); + + const effectivePrice = new BigNumber(tokenIn.amount).div(tokenOutAmount); + const priceImpact = effectivePrice.div(beforeSpotPriceInOverOut).minus(one); + + return priceImpact.toString(); +}; + +export const calcPriceImpactGivenOut = (tokenOut: Coin, tokenInDenom: string, pool: ExtendedPool) => { + const inPoolAsset = getPoolAsset(pool, tokenInDenom); + const outPoolAsset = getPoolAsset(pool, tokenOut.denom); + + const swapFee = new BigNumber(pool.poolParams?.swapFee || 0).shiftedBy(-18); + + const beforeSpotPriceInOverOut = calcSpotPrice( + new BigNumber(inPoolAsset.amount), + new BigNumber(inPoolAsset.weight), + new BigNumber(outPoolAsset.amount), + new BigNumber(outPoolAsset.weight), + swapFee, + ); + + const tokenInAmount = calcInGivenOut( + new BigNumber(inPoolAsset.amount), + new BigNumber(inPoolAsset.weight), + new BigNumber(outPoolAsset.amount), + new BigNumber(outPoolAsset.weight), + new BigNumber(tokenOut.amount), + swapFee, + ).decimalPlaces(0); + + const effectivePrice = new BigNumber(tokenInAmount).div(tokenOut.amount); + const priceImpact = effectivePrice.div(beforeSpotPriceInOverOut).minus(one); + + return priceImpact.toString(); +}; diff --git a/src/connectors/osmosis/osmosis.ts b/src/connectors/osmosis/osmosis.ts new file mode 100755 index 0000000000..e26aeec9ee --- /dev/null +++ b/src/connectors/osmosis/osmosis.ts @@ -0,0 +1,3082 @@ +// ts-expect-error cases: +//Case 1 - Asset.traces?: (IbcTransition | IbcCw20Transition | IbcBridgeTransition | NonIbcTransition)[]; +// it's mad about these not being cross-compatible between old asset and new asset (and thus my universal asset) +// not presently using traces, will ignore unless needed later (would be a lot of work though for something not in use) +// happens around tokenList + +//Case 2 - CosmosAsset.typeAsset = "str" | "str" +// new model added more values to this string enum, but +// these two are compatible (as constructor works in one direction) +// happens around assetList + +import { coin, Coin } from '@cosmjs/amino'; +import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'; +import { GeneratedType, Registry } from '@cosmjs/proto-signing'; +import { AminoTypes, SigningStargateClient, setupIbcExtension, GasPrice, calculateFee } from '@cosmjs/stargate'; +import { Tendermint37Client, HttpBatchClient } from '@cosmjs/tendermint-rpc'; +import { + convertDollarValueToCoins, + convertDollarValueToShares, + calcShareOutAmount, + makePoolPairs, +} from '@osmonauts/math'; +import type { PrettyPair } from '@osmonauts/math/esm/types'; +import { FEES } from '@osmonauts/utils'; +import { BigNumber } from 'bignumber.js'; +import fse from 'fs-extra'; +import { + cosmosAminoConverters, + cosmosProtoRegistry, + cosmwasmAminoConverters, + cosmwasmProtoRegistry, + ibcProtoRegistry, + ibcAminoConverters, + osmosisAminoConverters, + osmosisProtoRegistry, + osmosis, // OSMO message composer classes don't quite match up with what the RPC/Go backend actually accepts. + cosmos, +} from 'osmojs'; +import { PoolAsset } from 'osmojs/esm/osmosis/gamm/v1beta1/balancerPool'; + +import { CosmosWallet, cWalletMaker, CosmosTokenValue, CosmosBase } from '../../chains/cosmos/cosmos-base'; +import { getCoinGeckoPrices } from '../../chains/cosmos/cosmos.prices'; +import { CosmosAsset } from '../../chains/cosmos/cosmos.universaltypes'; +import { isValidCosmosAddress } from '../../chains/cosmos/cosmos.validators'; +import { + PositionInfo as AMMPositionInfo, + GetPositionInfoRequestType as AMMGetPositionInfoRequestType, + AddLiquidityRequestType as AMMAddLiquidityRequestType, + AddLiquidityResponseType as AMMAddLiquidityResponseType, + RemoveLiquidityRequestType as AMMRemoveLiquidityRequestType, + RemoveLiquidityResponseType as AMMRemoveLiquidityResponseType, +} from '../../schemas/amm-schema'; +import { TokensRequestType, TokensResponseType } from '../../schemas/chain-schema'; +import { + CollectFeesRequestType as CLMMCollectFeesRequestType, + CollectFeesResponseType as CLMMCollectFeesResponseType, + OpenPositionRequestType as CLMMOpenPositionRequestType, + OpenPositionResponseType as CLMMOpenPositionResponseType, + PositionInfo as CLMMPositionInfo, + GetPositionInfoRequestType as CLMMGetPositionInfoRequestType, + AddLiquidityRequestType as CLMMAddLiquidityRequestType, + AddLiquidityResponseType as CLMMAddLiquidityResponseType, + RemoveLiquidityRequestType as CLMMRemoveLiquidityRequestType, + RemoveLiquidityResponseType as CLMMRemoveLiquidityResponseType, + QuotePositionResponseType, + ExecuteSwapRequestType, +} from '../../schemas/clmm-schema'; +import { TokenInfo } from '../../services/base'; +import { percentRegexp } from '../../services/config-manager-v2'; +import { logger } from '../../services/logger'; +import { isFractionString } from '../../services/string-utils'; +import { walletPath } from '../../wallet/utils'; + +import { OsmosisConfig } from './osmosis.config'; +import { OsmosisController } from './osmosis.controllers'; +import { + parseFees, + extendPool, + getPoolByIdAndFilter, + getPoolByAddressAndFilter, + filterPoolsStableSwap, + filterPoolsCLMM, + filterPoolsGAMM, + CLMMMakePoolPairs, +} from './osmosis.pools.utils'; +import { + getRoutesForTrade, + calcAmountWithSlippage, + calcPriceImpactGivenIn, + calcPriceImpactGivenOut, +} from './osmosis.swap'; +import { + CoinGeckoToken, + CoinDenom, + Exponent, + CoinSymbol, + PriceHash, + OsmosisExpectedTrade, + ToLog_OsmosisExpectedTrade, + TransactionResponse, + OsmosisExpectedTradeRoute, + AddPositionTransactionResponse, + ReduceLiquidityTransactionResponse, + SerializableExtendedPool, + PriceAndSerializableExtendedPools, + ExtendedPool, + AnyPoolType, + TransferRequest, + TradeInfo, +} from './osmosis.types'; +import { findTickForPrice, tickToPrice, calculatePriceToTick } from './osmosis.utils'; + +export interface TokensRequest { + chain?: string; //the target chain (e.g. ethereum, avalanche, or harmony) + network?: string; // the target network of the chain (e.g. mainnet) + tokenSymbols?: string[]; +} + +export interface TokensResponse { + tokens: TokenInfo[]; +} + +const { joinPool, exitPool, joinSwapExternAmountIn, swapExactAmountIn } = + osmosis.gamm.v1beta1.MessageComposer.withTypeUrl; +const { createPosition, addToPosition, withdrawPosition, collectSpreadRewards, collectIncentives } = + osmosis.concentratedliquidity.v1beta1.MessageComposer.withTypeUrl; +const { send } = cosmos.bank.v1beta1.MessageComposer.fromPartial; + +const protoRegistry = [ + ...cosmosProtoRegistry, + ...cosmwasmProtoRegistry, + ...ibcProtoRegistry, + ...osmosisProtoRegistry, +] as unknown as ReadonlyArray<[string, GeneratedType]>; // differing versions of proto-signing.cosmojs + +const aminoConverters = { + ...cosmosAminoConverters, + ...cosmwasmAminoConverters, + ...ibcAminoConverters, + ...osmosisAminoConverters, +}; + +const registry = new Registry(protoRegistry); +const aminoTypes = new AminoTypes(aminoConverters); +const successfulTransaction = 0; +const exampleOsmosisPublicKey = 'osmo000000000000000000000000000000000000000'; + +export class Osmosis extends CosmosBase { + // Configuration + public config: OsmosisConfig.NetworkConfig; + + public controller; + private static _instances: { [name: string]: Osmosis }; + private _gasPrice: number; + private _nativeTokenSymbol: string; + private _chain: string; + private _network: string; + private _requestCount: number; + private _metricsLogInterval: number; + private _metricTimer; + public _walletAddressExample: string; + private signingClient?: any; + private lastCoinGeckoCallTime?: number; // ton of errors from coingecko from calling too often, especially during testing + private lastCoinGeckoPrices?: Record; + public gasLimitEstimate: number; + public readonly feeTier: string = 'medium'; // FEE_VALUES.osmosis[_feeTier] low medium high osmojs/src/utils/gas/values.ts + public allowedSlippage: string = '0/100'; + public manualGasPriceToken: string = 'uosmo'; + private tendermint37Client?: Tendermint37Client; + private httpBatchClient?: HttpBatchClient; + private constructor(network: string) { + const config = OsmosisConfig.config; + super( + network, + config.chainName(network), // osmo-test-5 or osmosis-1 + config.nodeURL(network).toString(), + config.gasAdjustment, + config.feeTier, + config.manualGasPriceToken, + config.gasLimitTransaction, + config.allowedSlippage, + 'osmosis', // rpc provider + config.useEIP1559DynamicBaseFeeInsteadOfManualGasPrice, + config.rpcAddressDynamicBaseFee, + config.manualGasPrice, + ); + this._network = network; + this._chain = 'cosmos'; + this._nativeTokenSymbol = config.nativeCurrencySymbol; + this._walletAddressExample = exampleOsmosisPublicKey; + this.manualGasPriceToken = config.manualGasPriceToken; + + this._gasPrice = Number(this.manualGasPrice); + this.feeTier = config.feeTier; + this.gasLimitEstimate = config.gasLimitTransaction; + this.allowedSlippage = config.allowedSlippage; + + this.chainName = config.chainName(network); + this.signingClient = undefined; + this.controller = OsmosisController; + + this._requestCount = 0; + this._metricsLogInterval = 300000; // 5 minutes + + this._metricTimer = setInterval(this.metricLogger.bind(this), this.metricsLogInterval); + logger.info(`Initialized Osmosis connector for network: ${network}, nodeURL: ${this.nodeURL}`); + } + + public static getInstance(network?: string): Osmosis { + if (!network) network = 'mainnet'; + if (Osmosis._instances === undefined) { + Osmosis._instances = {}; + } + if (!(network in Osmosis._instances)) { + Osmosis._instances[network] = new Osmosis(network); + } + + return Osmosis._instances[network]; + } + + public static getConnectedInstances(): { [name: string]: Osmosis } { + return Osmosis._instances; + } + + public requestCounter(msg: any): void { + if (msg.action === 'request') this._requestCount += 1; + } + + public metricLogger(): void { + logger.info(this.requestCount + ' request(s) sent in last ' + this.metricsLogInterval / 1000 + ' seconds.'); + this._requestCount = 0; // reset + } + /** + * Validate Cosmos address format + * @param address The address to validate + * @returns The address if valid + * @throws Error if the address is invalid + */ + public static validateAddress(address: string): string { + try { + // Attempt to validate the address + if (isValidCosmosAddress(address)) { + return address; + } + } catch (e) { + logger.warn(`Invalid Cosmos/Osmosis address found in wallet directory: ${address}`); + return null; + } + } + public get provider() { + return this._provider; + } + + public get gasPrice(): number { + return this._gasPrice; + } + + public get chain(): string { + return this._chain; + } + + public get nativeTokenSymbol(): string { + return this._nativeTokenSymbol; + } + + public get requestCount(): number { + return this._requestCount; + } + + public get metricsLogInterval(): number { + return this._metricsLogInterval; + } + + async getDenomMetadata(provider: any, denom: string): Promise { + return await provider.cosmos.bank.denomMetadata(denom); + } + + async close() { + clearInterval(this._metricTimer); + if (this._chain in Osmosis._instances) { + delete Osmosis._instances[this._chain]; + } + } + + public ready(): boolean { + return this._ready; + } + + private async osmosisGetSigningStargateClient(rpcEndpoint: string, signer: any) { + this.osmosisGetHttpBatchClient(rpcEndpoint); + await this.osmosisGetTendermint37Client(); + + const signingStargateClient = await SigningStargateClient.createWithSigner(this.tendermint37Client!, signer, { + //@ts-expect-error differing versions of proto-signing.cosmojs + registry: registry, + aminoTypes: aminoTypes, + }); + + return signingStargateClient; + } + + private async osmosisGetTendermint37Client() { + this.tendermint37Client = await Tendermint37Client.create(this.httpBatchClient!); + } + + private osmosisGetHttpBatchClient(rpcEndpoint: string) { + this.httpBatchClient = new HttpBatchClient(rpcEndpoint, { + dispatchInterval: 2000, + }); + } + + private async getCoinGeckPricesStored() { + if ( + !this.lastCoinGeckoPrices || + !this.lastCoinGeckoCallTime || + this.lastCoinGeckoCallTime + 60 * 1000 * 3 < Date.now() + ) { + // 3 minutes + this.lastCoinGeckoCallTime = Date.now(); + this.lastCoinGeckoPrices = await getCoinGeckoPrices(this.tokenList); + } + return this.lastCoinGeckoPrices; + } + + public getTokenByAddress(address: string): CosmosAsset { + const token = this.tokenList.find((token: CosmosAsset) => token.address === address); + if (!token) { + throw new Error('Osmosis token not found for address: ' + address); + } + return token; + } + public getDenomForCoinGeckoId = (coinGeckoId: CoinGeckoToken): CoinDenom => { + const asset_found = this.tokenList.find((asset) => asset.coingeckoId === coinGeckoId); + if (asset_found) { + return asset_found.base; + } else { + return ''; + } + }; + public osmoDenomToSymbol = (denom: CoinDenom): CoinSymbol => { + const asset = this.getTokenByBase(denom); + const symbol = asset?.symbol; + if (!symbol) { + return denom; + } + return symbol; + }; + public symbolToOsmoDenom = (token: CoinSymbol): CoinDenom => { + const asset = this.tokenList.find(({ symbol }) => symbol === token); + const base = asset?.base; + if (!base) { + console.log(`cannot find base for token ${token}`); + return ''; + } + return base; + }; + + // technically by base + public getExponentByBase = (denom: CoinDenom): Exponent => { + const asset = this.getTokenByBase(denom); + if (asset && asset.denomUnits) { + const unit = asset.denomUnits.find(({ denom }) => denom === asset.display); + if (unit) { + return unit.exponent; + } + } + return 0; + }; + public noDecimals = (num: number | string) => { + return new BigNumber(num).decimalPlaces(0, BigNumber.ROUND_DOWN).toString(); + }; + public baseUnitsToDollarValue = (prices: PriceHash, symbol: string, amount: string | number) => { + const denom = this.symbolToOsmoDenom(symbol); + return new BigNumber(amount).shiftedBy(-this.getExponentByBase(denom)).multipliedBy(prices[denom]).toString(); + }; + public dollarValueToDenomUnits = (prices: PriceHash, symbol: string, value: string | number) => { + const denom = this.symbolToOsmoDenom(symbol); + return new BigNumber(value).dividedBy(prices[denom]).shiftedBy(this.getExponentByBase(denom)).toString(); + }; + public baseUnitsToDisplayUnits = (symbol: string, amount: string | number) => { + const denom = this.symbolToOsmoDenom(symbol); + return new BigNumber(amount).shiftedBy(-this.getExponentByBase(denom)).toString(); + }; + + public isEmptyArray = (arr: any[]) => arr.length === 0; + + // Add new method to get first wallet address + public static async getFirstWalletAddress(): Promise { + const path = `${walletPath}/cosmos`; + try { + // Create directory if it doesn't exist + await fse.ensureDir(path); + + // Get all .json files in the directory + const files = await fse.readdir(path); + const walletFiles = files.filter((f) => f.endsWith('.json')); + + if (walletFiles.length === 0) { + return null; + } + + // Get the first wallet address (without .json extension) + const walletAddress = walletFiles[0].slice(0, -5); + + try { + // Attempt to validate the address + if (isValidCosmosAddress(walletAddress)) { + return walletAddress; + } + } catch (e) { + logger.warn(`Invalid Cosmos/Osmosis address found in wallet directory: ${walletAddress}`); + return null; + } + } catch (err) { + return null; + } + } + + /** + * Get a wallet address example for schema documentation + */ + public static async getWalletAddressExample(): Promise { + const defaultAddress = exampleOsmosisPublicKey; + 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 Cosmos/Osmosis wallet address for example: ${error.message}`); + return defaultAddress; + } + } + + async getBalances(wallet: CosmosWallet): Promise> { + const balances: Record = {}; + const accounts = await wallet.member.getAccounts(); + const { address } = accounts[0]; + + const balancesContainer = await this._provider.cosmos.bank.v1beta1.allBalances({ + address: address, + pagination: { + key: new Uint8Array(), + offset: BigInt(0), + limit: BigInt(10000), + countTotal: false, + reverse: false, + }, + }); + + const allTokens = balancesContainer.balances; + + await Promise.all( + allTokens.map(async (t: { denom: string; amount: string }) => { + let token = this.getTokenByBase(t.denom); + + try { + if (!token && t.denom.startsWith('ibc/')) { + const ibcHash: string = t.denom.replace('ibc/', ''); + + // Get base denom by IBC hash + if (ibcHash) { + const { denomTrace } = await setupIbcExtension(this._provider).ibc.transfer.denomTrace(ibcHash); + // tried calling with cosmos provider/versions. QueryDenomTraces failing: "base.queryAbci is not a function" + if (denomTrace) { + const { baseDenom } = denomTrace; + token = this.getTokenByBase(baseDenom); + } + } + } + } catch (err) { + //can skip this - will be added by raw denom + } + + // Not all tokens are added in the registry so we use the denom if the token doesn't exist + balances[token ? token.symbol : t.denom] = { + value: new BigNumber(t.amount), + decimals: token && token.decimals ? token.decimals : token ? this.getTokenDecimals(token) : 6, + }; + }), + ); + + return balances; + } + + // used with signingResponse or via getTransaction + async dissectTransactionResponse(address: string, signingResponse = undefined, getTxInput = undefined) { + if (signingResponse == undefined) { + signingResponse = getTxInput.txResponse; + } + const tokenBalanceChangesDenoms: Record = {}; + const tokenBalanceChanges: Record = {}; + const coin_spents: string[] = []; + const coin_receiveds: string[] = []; + let new_position_id = ''; + let position_id = ''; + // dissect final balance changes + try { + const denom_list = []; + if (address) { + signingResponse.events.forEach((event) => { + if (event.attributes[0].value == address) { + if (event.type == 'coin_spent') { + event.attributes[1].value.split(',').forEach((e) => { + coin_spents.push(e); + }); + } else if (event.type == 'coin_received') { + event.attributes[1].value.split(',').forEach((e) => { + coin_receiveds.push(e); + }); + } + } + }); + coin_receiveds.forEach((coin_spent) => { + //eg. 100199uosmo '12uion,16017uosmo' + let amount = ''; + let denom = ''; + let reading_amount = true; + [...coin_spent].forEach((c) => { + if (reading_amount && c >= '0' && c <= '9') { + amount += c; + } else { + reading_amount = false; + denom += c; + } + }); + if (amount != '' && denom != '') { + tokenBalanceChangesDenoms[denom] = tokenBalanceChangesDenoms[denom] + ? tokenBalanceChangesDenoms[denom] + Number(amount) + : Number(amount); + if (!denom_list.includes(denom)) { + denom_list.push(denom); + } + } + }); + + coin_spents.forEach((coins) => { + let amount = ''; + let denom = ''; + let reading_amount = true; + [...coins].forEach((c) => { + if (reading_amount && c >= '0' && c <= '9') { + amount += c; + } else { + reading_amount = false; + denom += c; + } + }); + + if (amount != '' && denom != '') { + if (!denom_list.includes(denom)) { + denom_list.push(denom); + } + tokenBalanceChangesDenoms[denom] = tokenBalanceChangesDenoms[denom] + ? tokenBalanceChangesDenoms[denom] - Number(amount) + : Number(amount) * -1; + } + }); + + denom_list.forEach((denom) => { + const decimals = this.getExponentByBase(denom); + tokenBalanceChanges[this.osmoDenomToSymbol(denom)] = new BigNumber(tokenBalanceChangesDenoms[denom]) + .shiftedBy(decimals * -1) + .decimalPlaces(decimals) + .toNumber(); + }); + } + } catch (error) { + console.debug(error); + } + + // find new_position_id for addLiquidityCLMM (we use this instead of position address) + try { + signingResponse.events.forEach((event) => { + event.attributes.forEach((attr) => { + if (attr.key == 'new_position_id') { + new_position_id = attr.value; + } else if (attr.key == 'position_id') { + position_id = attr.value; + } + }); + }); + } catch (error) { + console.debug(error); + } + const return_position_id = new_position_id != '' ? new_position_id : position_id; + return [tokenBalanceChangesDenoms, tokenBalanceChanges, return_position_id]; + } + + /** + * Given the amount of `baseToken` to put into a transaction, calculate the + * amount of `quoteToken` that can be expected from the transaction. + * + * This is typically used for calculating token buy/sell prices. + * + * @param network testnet/mainnet + * @param baseToken Token input for the transaction + * @param quoteToken Output from the transaction + * @param amount Amount of `baseToken` to put into the transaction + * @param tradeType "BUY" or "SELL" + * @param poolType amm/clmm + * @param poolAddress? Specific pool address to use for the trade + * @param allowedSlippagePercent? Allowed slippage eg "1%" + * @param feeTier_input? low/medium/high, overwrites feeTier specified in .yml + * @param gasAdjustment_input? Gas offered multiplier, overwrites gasAdjustment specified in .yml + */ + async estimateTrade( + network: string, + baseToken: CosmosAsset, + quoteToken: CosmosAsset, + amount: BigNumber, + tradeType: string, + poolType: string, + poolAddress?: string, + allowedSlippagePercent?: number, + feeTier_input?: string, + gasAdjustment_input?: number, + ): Promise { + let slippage_percent = 0; + if (allowedSlippagePercent) { + slippage_percent = allowedSlippagePercent; + } + + let feeTier = this.feeTier; + if (feeTier_input) { + feeTier = feeTier_input; + } + let gasAdjustment = this.gasAdjustment; + if (gasAdjustment_input) { + gasAdjustment = gasAdjustment_input; + } + + if (tradeType == 'BUY') { + // change to SELL? + //swap base and quotetokens + const realBaseToken = quoteToken; + quoteToken = baseToken; + baseToken = realBaseToken; + } + + logger.info(`Fetching pair data for ${quoteToken.symbol}-${baseToken.symbol}.`); + + const prices = await this.getCoinGeckPricesStored(); + if (this.isEmptyArray(Object.keys(prices))) { + throw new Error(`Osmosis: Failed to retrieve prices for tokens given.`); + } + + let poolsContainer; + try { + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } catch (err) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } + const poolsDataAll: ExtendedPool[] = poolsContainer.pools; + let pools: ExtendedPool[] = []; + // make sure we actually have prices and tokens for the pool you're looking for + if (poolAddress) { + const verifyAssets = true; + // @ts-expect-error: Case 1 + pools = getPoolByAddressAndFilter(this.tokenList, poolsDataAll, prices, poolAddress, verifyAssets); + } else { + switch (poolType.toLowerCase()) { + case 'router': + // @ts-expect-error: Case 1 + pools = filterPoolsStableSwap(this.tokenList, poolsDataAll, prices); + break; + case 'clmm': + // @ts-expect-error: Case 1 + pools = filterPoolsCLMM(this.tokenList, poolsDataAll, prices); + break; + case 'amm': + // @ts-expect-error: Case 1 + pools = filterPoolsGAMM(this.tokenList, poolsDataAll, prices); + break; + default: + throw new Error(`Osmosis: poolType was not provided. How did you get here?`); + } + } + if (this.isEmptyArray(pools)) { + throw new Error(`Osmosis: Failed to retrieve pools for tokens and pooltype given.`); + } + + let pairs: PrettyPair[] = []; + if (!this.isEmptyArray(pools) && !this.isEmptyArray(Object.keys(prices))) { + if (poolType.toLocaleLowerCase() == 'clmm') { + pairs = CLMMMakePoolPairs(this.tokenList, pools); + } else { + // @ts-expect-error: Osmosis Case 2 + pairs = makePoolPairs(this.assetList, pools, prices); + } + } + + // eg. token=OSMO, token.base=uOSMO, so swap calcs are done in uosmo is + const tokenInAmount = new BigNumber(amount).shiftedBy(baseToken.decimals).toString(); + + const tokenInDollarValue = new BigNumber(amount || '0').multipliedBy(prices[baseToken.base]); + + const toTokenDollarPrice = prices[quoteToken.base]; + let toTokenAmount; + if (toTokenDollarPrice) { + toTokenAmount = tokenInDollarValue.div(toTokenDollarPrice); + } else { + // no price found for quote token - maybe should throw here but let's see if there's a pool route[] for it + toTokenAmount = 0; + } + + const tokenOutAmount = new BigNumber(toTokenAmount) + .shiftedBy(quoteToken.decimals) + // tokenOut defined by .base (eg. uION) + .toString(); + + let tokenOutAmountAfterSlippage; + if (slippage_percent == 100 && network != 'testnet') { + tokenOutAmountAfterSlippage = '1'; // just in case someone tries to scew themselves + } else { + tokenOutAmountAfterSlippage = calcAmountWithSlippage(tokenOutAmount, slippage_percent); + } + + const tokenIn = { + denom: baseToken.base, + amount: tokenInAmount, + }; + const tokenOut = { + denom: quoteToken.base, + amount: tokenOutAmountAfterSlippage, + }; + const routes = getRoutesForTrade(this.assetList, { + trade: { + sell: { + denom: tokenIn.denom, + amount: tokenIn.amount, + }, + buy: { + denom: tokenOut.denom, + amount: tokenOut.amount, + }, + }, + pairs, + }); + + if (!routes || routes.length === 0 || routes.length > 2) { + logger.info( + `No trade routes found for ${quoteToken.symbol}-${baseToken.symbol} ${quoteToken.symbol}-${baseToken.symbol}`, + ); + throw new Error( + `No trade routes found for ${quoteToken.symbol}-${baseToken.symbol} ${quoteToken.symbol}-${baseToken.symbol}`, + ); + } + + // so far we have pools, routes, and token info... + let route_length_1_pool_swapFee = ''; + let priceImpact = '0'; + if (new BigNumber(tokenIn.amount).isEqualTo(0)) { + priceImpact = '0'; + } else if (routes.length === 1) { + const route_length_1_pool = pools.find((pool) => pool.id === routes[0].poolId)!; // take first route - these are sorted by liquidity already + if (poolType.toLowerCase() != 'clmm') { + priceImpact = calcPriceImpactGivenIn(tokenIn, tokenOut.denom, route_length_1_pool); + } + route_length_1_pool_swapFee = new BigNumber(route_length_1_pool.poolParams?.swapFee || 0).toString(); // .shiftedBy(-16) shift used in CCA + } else { + // THIS ASSUMES length == 2 - per CCA/osmosis guys.. + const tokenInRoute = routes.find((route) => route.tokenOutDenom !== tokenOut.denom)!; + const tokenOutRoute = routes.find((route) => route.tokenOutDenom === tokenOut.denom)!; + + const tokenInPool = pools.find((pool) => pool.id === tokenInRoute.poolId)!; + const tokenOutPool = pools.find((pool) => pool.id === tokenOutRoute.poolId)!; + let priceImpactIn = '0'; + let priceImpactOut = '0'; + if (poolType.toLowerCase() != 'clmm') { + priceImpactIn = calcPriceImpactGivenIn(tokenIn, tokenInRoute.tokenOutDenom, tokenInPool); + priceImpactOut = calcPriceImpactGivenOut(tokenOut, tokenOutRoute.tokenOutDenom, tokenOutPool); + } + + priceImpact = new BigNumber(priceImpactIn).plus(priceImpactOut).toString(); + } + + // routes.length=1 mean there's just 1 hop - we're always just given one potentially route[] for a trade route request + let swapRoutes: OsmosisExpectedTradeRoute[] = []; + + if (routes.length === 1) { + swapRoutes = routes.map((route) => { + return { + poolId: route.poolId.toString(), + swapFee: route_length_1_pool_swapFee, + baseLogo: baseToken.logoURIs, + baseSymbol: baseToken.symbol, + quoteLogo: quoteToken.logoURIs, + quoteSymbol: quoteToken.symbol, + tokenOutDenom: tokenOut.denom, + }; + }); + } else { + const swapFees: BigNumber[] = []; + swapRoutes = routes + .map((route) => { + const pool = pools.find((pool) => pool.id === route.poolId); + let baseAsset: CosmosAsset; + let quoteAsset: CosmosAsset; + if (route.tokenOutDenom !== tokenOut.denom) { + baseAsset = baseToken; + quoteAsset = this.getTokenByBase(route.tokenOutDenom)!; + } else { + const tokenInDenom = pool.poolAssets.find(({ token }) => token.denom !== tokenOut.denom).token.denom!; + baseAsset = this.getTokenByBase(tokenInDenom)!; + quoteAsset = quoteToken; + } + let fee = new BigNumber(0); + if (pool.poolParams && pool.poolParams.swapFee) { + fee = new BigNumber(pool.poolParams.swapFee); + } // .shiftedBy(-16) shift used in CCA + + swapFees.push(fee); + return { + poolId: route.poolId.toString(), + swapFee: fee, + baseLogo: baseAsset.logoURIs, + baseSymbol: baseAsset.symbol, + quoteLogo: quoteAsset.logoURIs, + quoteSymbol: quoteAsset.symbol, + tokenOutDenom: route.tokenOutDenom, + }; + }) + .map((route) => { + const totalFee = swapFees.reduce((total, cur) => total.plus(cur), new BigNumber(0)); + const highestFee = swapFees.sort((a, b) => (a.lt(b) ? 1 : -1))[0]; + const feeRatio = highestFee.div(totalFee); + return { + ...route, + swapFee: route.swapFee.multipliedBy(feeRatio).toString() + '%', + }; + }); + } + + const expectedOutputAfterSlippage = tokenOutAmountAfterSlippage; + + // can't simulate here without address/signingclient + const feeObject = FEES.osmosis.swapExactAmountIn(feeTier); + const gasLimitEstimate = new BigNumber(feeObject.gas).multipliedBy(gasAdjustment); + + if (gasLimitEstimate.gt(new BigNumber(this.gasLimitEstimate))) { + logger.error( + `Osmosis: Gas limit exceeded ${gasLimitEstimate.toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + throw new Error( + `Osmosis: Gas limit exceeded ${gasLimitEstimate.toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + } + + const expectedAmountNum = new BigNumber(expectedOutputAfterSlippage); + const tokenInAmountNum = new BigNumber(amount).shiftedBy(baseToken.decimals); + const executionPrice = expectedAmountNum.div(tokenInAmountNum); + + logger.info( + `Best trade for ${quoteToken.address}-${baseToken.address}: ${ToLog_OsmosisExpectedTrade({ + gasUsed: '', + gasWanted: '', + routes: swapRoutes, + tokenOutAmount: tokenOutAmount, + tokenOutAmountAfterSlippage: expectedOutputAfterSlippage, + executionPrice: executionPrice, + gasLimitEstimate: new BigNumber(gasLimitEstimate), + tokenInDenom: tokenIn.denom, + tokenInAmount: tokenInAmount, + tokenInAmountAfterSlippage: tokenInAmount, // this shouldn't change (unless we like, super hammer every pool I guess), not sure what uniswap guys are doing with it + tokenOutDenom: tokenOut.denom, + priceImpact: Number(priceImpact), + })}`, + ); + + return { + gasUsed: '', + gasWanted: '', + routes: swapRoutes, + tokenOutAmount: tokenOutAmount, + tokenOutAmountAfterSlippage: expectedOutputAfterSlippage, + executionPrice: executionPrice, + gasLimitEstimate: new BigNumber(gasLimitEstimate), + tokenInDenom: tokenIn.denom, + tokenInAmount: tokenInAmount, + tokenInAmountAfterSlippage: tokenInAmount, // this shouldn't change (unless we like, super hammer every pool I guess), not sure what uniswap guys are doing with it + tokenOutDenom: tokenOut.denom, + priceImpact: Number(priceImpact), + }; // == OsmosisExpectedTrade + } + + /** + * Given a wallet and a Cosmosish trade, try to execute it on blockchain. + * + * @param network testnet/mainnet + * @param wallet CosmosWallet + * @param trade Expected trade + * @param req ExecuteSwapRequestType + * @param trade Osmosis TradeInfo type + * @param feeTier_input? low/medium/high, overwrites feeTier specified in .yml + * @param gasAdjustment_input? Gas offered multiplier, overwrites gasAdjustment specified in .yml + */ + async executeTrade( + network: string, + wallet: CosmosWallet, + req: ExecuteSwapRequestType, + trade: TradeInfo, + feeTier_input?: string, + gasAdjustment_input?: number, + ): Promise { + let slippage_percent = 0; + if (req.slippagePct) { + slippage_percent = req.slippagePct; + } else { + slippage_percent = this.getAllowedSlippage(this.allowedSlippage); + } + let feeTier = this.feeTier; + if (feeTier_input) { + feeTier = feeTier_input; + } + let gasAdjustment = this.gasAdjustment; + if (gasAdjustment_input) { + gasAdjustment = gasAdjustment_input; + } + + const keyWallet = await cWalletMaker(wallet.privkey, 'osmo'); + this.signingClient = await this.osmosisGetSigningStargateClient(this.nodeURL, keyWallet.member); + + const routes = trade.expectedTrade.routes; + + let tokenOutMinAmount; + if (slippage_percent == 100 || network == 'testnet') { + tokenOutMinAmount = 1; + } else { + tokenOutMinAmount = this.noDecimals( + (Number(trade.expectedTrade.tokenOutAmount) * (100 - slippage_percent)) / 100, + ); + } + + const msg = swapExactAmountIn({ + sender: req.walletAddress, + // @ts-expect-error: bad osmojs models + routes: routes, + tokenIn: coin(trade.expectedTrade.tokenInAmount, trade.expectedTrade.tokenInDenom), + tokenOutMinAmount: tokenOutMinAmount.toString(), + }); + + const enumFee = FEES.osmosis.swapExactAmountIn(feeTier); + let gasToUse = enumFee.gas; + try { + const gasEstimation = await this.signingClient.simulate(req.walletAddress, [msg]); + gasToUse = gasEstimation; + } catch (error1) { + const error = error1 as Error; + if (error.message.includes('token is lesser than min amount')) { + logger.error( + `Osmosis: Amount less than min amount error. tokenOutMinAmount: ${tokenOutMinAmount.toString()}.`, + ); + throw new Error( + `Osmosis: Amount less than min amount error. tokenOutMinAmount: ${tokenOutMinAmount.toString()}.`, + ); + } else { + logger.error(`Osmosis: Simulate failed.`); + logger.error(error); + throw error; + } + } + + const gasPrice = await this.getLatestBasePrice(); + const calcedFee = calculateFee( + Math.round(Number(gasToUse) * (gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + + if (new BigNumber(calcedFee.gas).gt(new BigNumber(this.gasLimitEstimate))) { + logger.error( + `Osmosis: Gas limit exceeded ${new BigNumber(calcedFee.gas).toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + throw new Error( + `Osmosis: Gas limit exceeded ${new BigNumber(calcedFee.gas).toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + } + + try { + const res: TransactionResponse = await this.signingClient.signAndBroadcast(req.walletAddress, [msg], calcedFee); + res.gasPrice = gasPrice; + res.gasWanted = new BigNumber(calcedFee.gas).toString(); + res.gasUsed = new BigNumber(calcedFee.gas).toString(); + res.feeAmount = calcedFee.amount[0]['amount']; + + return res; + } catch (error) { + console.debug(error); + } finally { + this.signingClient.disconnect(); + } + + logger.error('Osmosis: Trade execution failed, reason unknown.'); + throw new Error('Osmosis: Trade execution failed, reason unknown.'); + } + + /** + * Given a 2 token symbols and 1-2 amounts, exchange amounts for pool liquidity shares + * + * @param wallet CosmosWallet + * @param req AMM - AddLiquidityRequestType + * @param feeTier_input? low/medium/high, overwrites feeTier specified in .yml + * @param gasAdjustment_input? Gas offered multiplier, overwrites gasAdjustment specified in .yml + */ + async addLiquidityAMM( + wallet: CosmosWallet, + req: AMMAddLiquidityRequestType, // (poolAddress and baseTokenAmount and quoteTokenAmount) OR (baseToken and quoteToken and baseTokenAmount and quoteTokenAmount) + feeTier_input?: string, + gasAdjustment_input?: number, + ): Promise<[AddPositionTransactionResponse, AMMAddLiquidityResponseType]> { + if (!req.poolAddress) { + throw new Error('Osmosis: addLiquidityAMM Request is missing required fields.'); + } + if (!req.baseTokenAmount && !req.quoteTokenAmount) { + throw new Error('Osmosis: addLiquidityAMM Request is missing required fields.'); + } + + const poolAddress = req.poolAddress; + let baseToken: CosmosAsset; + let quoteToken: CosmosAsset; + const amount0 = req.baseTokenAmount ? req.baseTokenAmount : 0; + const amount1 = req.quoteTokenAmount ? req.quoteTokenAmount : 0; + + let signature: string = ''; + let fee: number = 0; + + const slippage_percent = req.slippagePct ? req.slippagePct : this.getAllowedSlippage(this.allowedSlippage); + let feeTier = this.feeTier; + if (feeTier_input) { + feeTier = feeTier_input; + } + let gasAdjustment = this.gasAdjustment; + if (gasAdjustment_input) { + gasAdjustment = gasAdjustment_input; + } + + try { + const keyWallet = await cWalletMaker(wallet.privkey, 'osmo'); + this.signingClient = await this.osmosisGetSigningStargateClient(this.nodeURL, keyWallet.member); + + if (!this.signingClient || !req.walletAddress) { + logger.error( + "Osmosis: addPositionAMM failed: Can't instantiate signing client. StargateClient undefined or address undefined.", + ); + throw new Error( + "Osmosis: addPositionAMM failed: Can't instantiate signing client. StargateClient undefined or address undefined.", + ); + } + + const prices = {}; //await this.getCoinGeckPricesStored(); + const poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + const poolsDataAll: ExtendedPool[] = poolsContainer.pools; + let pools: ExtendedPool[] = []; + const verifyAssets = false; + if (poolAddress) { + // @ts-expect-error: Case 1 + pools = getPoolByAddressAndFilter(this.tokenList, poolsDataAll, prices, poolAddress, verifyAssets); + } else { + // @ts-expect-error: Case 1 + pools = filterPoolsGAMM(this.tokenList, poolsDataAll, prices, verifyAssets); + } + + if (this.isEmptyArray(pools)) { + logger.error(`Osmosis: addPositionAMM Failed to retrieve pools for tokens and/or ID given.`); + throw new Error(`Osmosis: addPositionAMM Failed to retrieve pools for tokens and/or ID given.`); + } + + let amount0_bignumber = new BigNumber(0); + let amount1_bignumber = new BigNumber(0); + if (amount0) { + amount0_bignumber = new BigNumber(amount0); + } + if (amount1) { + amount1_bignumber = new BigNumber(amount1); + } + if (amount0_bignumber.isEqualTo(0) && amount1_bignumber.isEqualTo(0)) { + logger.error('Osmosis: addPositionAMM failed: Both token amounts equal to 0.'); + throw new Error('Osmosis: addPositionAMM failed: Both token amounts equal to 0.'); + } + + let singleToken_UseWhich: string | null = null; + if (!amount0_bignumber.isEqualTo(0) && amount1_bignumber.isEqualTo(0)) { + singleToken_UseWhich = '0'; + } + if (amount0_bignumber.isEqualTo(0) && !amount1_bignumber.isEqualTo(0)) { + singleToken_UseWhich = '1'; + } + // NOT CHECKING (local wallet) BALANCES HERE it will bounce back either way + + // now find the poolAddress for this pair + // const foundPools: any[] = []; + // not using filter here for price/etc + const pool: ExtendedPool = pools.find(({ address }) => address == poolAddress); + if (!baseToken || !quoteToken) { + if (pool.poolAssets) { + if (!baseToken) { + baseToken = this.getTokenByBase(pool.poolAssets[0].token.denom)!; + } + if (!quoteToken) { + quoteToken = this.getTokenByBase(pool.poolAssets[1].token.denom)!; + } + } else { + if (!baseToken) { + baseToken = this.getTokenByBase(pool.token0)!; + } + if (!quoteToken) { + quoteToken = this.getTokenByBase(pool.token1)!; + } + } + } + + let calcedFee; + if (pool) { + const gasPrice = await this.getLatestBasePrice(); + const msgs = []; + if (singleToken_UseWhich) { + // in case 1 of the amounts == 0 + let singleToken_amount = new BigNumber(0); + let singleToken: CosmosAsset | undefined = undefined; + if (singleToken_UseWhich == '0') { + singleToken_amount = amount0_bignumber; + singleToken = baseToken; + } else { + singleToken_amount = amount1_bignumber; + singleToken = quoteToken; + } + const inputCoin = { + denom: singleToken.base, + amount: singleToken_amount.shiftedBy(this.getExponentByBase(singleToken.base)).toString(), + }; + + const coinSymbol = singleToken.symbol; + const inputValue = this.baseUnitsToDollarValue(prices, coinSymbol, singleToken_amount.toNumber()); + // @ts-expect-error: Osmosis Case 2 - CosmosAsset.typeAsset + const coinsNeeded = convertDollarValueToCoins(this.assetList, inputValue, pool, prices); + // @ts-expect-error: Osmosis Case 3 + const shareOutAmount = calcShareOutAmount(pool, coinsNeeded); + + let finalShareOutAmount; + if (slippage_percent == 100) { + finalShareOutAmount = new BigNumber(1).integerValue(BigNumber.ROUND_CEIL); + } else { + finalShareOutAmount = new BigNumber(calcAmountWithSlippage(shareOutAmount, slippage_percent)).integerValue( + BigNumber.ROUND_CEIL, + ); + } + + const joinSwapExternAmountInMsg = joinSwapExternAmountIn({ + //@ts-expect-error: bad osmojs models + poolId: pool.id.toString(), + sender: req.walletAddress, + tokenIn: inputCoin, + shareOutMinAmount: this.noDecimals(finalShareOutAmount.toString()), + }); + msgs.push(joinSwapExternAmountInMsg); + + const enumFee = FEES.osmosis.joinSwapExternAmountIn(feeTier); + let gasToUse = enumFee.gas; + try { + const gasEstimation = await this.signingClient.simulate(req.walletAddress, msgs); + gasToUse = gasEstimation; + } catch (error1) { + const error = error1 as Error; + if (error.message.includes('token is lesser than min amount')) { + logger.error( + `Osmosis: Amount less than min amount error. tokenOutMinAmount: ${finalShareOutAmount.toString()}.`, + ); + throw new Error( + `Osmosis: Amount less than min amount error. tokenOutMinAmount: ${finalShareOutAmount.toString()}.`, + ); + } else { + logger.error(`Osmosis: Simulate failed.`); + logger.error(error); + throw error; + } + } + calcedFee = calculateFee( + Math.round(Number(gasToUse) * (gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + } else { + const allCoins = []; + allCoins.push({ + denom: baseToken.base, + amount: new BigNumber(amount0).shiftedBy(this.getExponentByBase(baseToken.base)).toString(), + }); + allCoins.push({ + denom: quoteToken.base, + amount: new BigNumber(amount1).shiftedBy(this.getExponentByBase(quoteToken.base)).toString(), + }); + + // alphabetize the coins going in or else invalid coins error + if (!(baseToken.base.toLowerCase() < quoteToken.base.toLowerCase())) { + allCoins.reverse(); + } + + // @ts-expect-error: Osmosis Case 3 + const shareOutAmount = calcShareOutAmount(pool, allCoins); + const tokenInMaxs = allCoins.map((c: Coin) => { + return coin(c.amount, c.denom); + }); + + let finalShareOutAmount; + if (slippage_percent == 100) { + finalShareOutAmount = new BigNumber(1).integerValue(BigNumber.ROUND_CEIL); + } else { + finalShareOutAmount = new BigNumber(calcAmountWithSlippage(shareOutAmount, slippage_percent)).integerValue( + BigNumber.ROUND_CEIL, + ); + } + + const joinPoolMsg = joinPool({ + //@ts-expect-error: bad osmojs models + poolId: pool.id.toString(), + sender: req.walletAddress, + shareOutAmount: this.noDecimals(finalShareOutAmount.toString()), + tokenInMaxs, + }); + msgs.push(joinPoolMsg); + + const enumFee = FEES.osmosis.joinPool(feeTier); + let gasToUse = enumFee.gas; + try { + // we only get gas back from simulating our tx... but Eth seems to be able to produce full quotes for pool joins + const gasEstimation = await this.signingClient.simulate(req.walletAddress, msgs); + gasToUse = gasEstimation; + } catch (error1) { + const error = error1 as Error; + if (error.message.includes('token is lesser than min amount')) { + logger.error( + `Osmosis: Amount less than min amount error. shareOutAmount: ${finalShareOutAmount.toString()}.`, + ); + throw new Error( + `Osmosis: Amount less than min amount error. shareOutAmount: ${finalShareOutAmount.toString()}.`, + ); + } else { + logger.error(`Osmosis: Simulate failed.`); + logger.error(error); + throw error; + } + } + + calcedFee = calculateFee( + Math.round(Number(gasToUse) * (gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + } + + if (new BigNumber(calcedFee.gas).gt(new BigNumber(this.gasLimitEstimate))) { + logger.error( + `Osmosis: Gas limit exceeded ${new BigNumber(calcedFee.gas).toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + throw new Error( + `Osmosis: Gas limit exceeded ${new BigNumber(calcedFee.gas).toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + } + + const signingResponse: AddPositionTransactionResponse = await this.signingClient.signAndBroadcast( + req.walletAddress, + msgs, + calcedFee, + ); + this.signingClient.disconnect(); + + if (signingResponse?.code !== successfulTransaction) { + signature = signingResponse.transactionHash; + fee = signingResponse.feeAmount ? Number(signingResponse.feeAmount) : 0; + const ammResponseF = { + signature, + status: signingResponse.code, // failed tx + data: { + fee, + baseTokenAmountAdded: 0, + quoteTokenAmountAdded: 0, + }, + }; + + return [signingResponse, ammResponseF]; + } + + let tokenBalanceChanges: Record = {}; + { + const dissectRes = (await this.dissectTransactionResponse(wallet.address, signingResponse)) as [ + Record, + Record, + string, + ]; + tokenBalanceChanges = dissectRes[1]; + } + + fee = signingResponse.feeAmount ? Number(signingResponse.feeAmount) : 0; + const baseTokenAmountAdded = tokenBalanceChanges[baseToken.symbol] + ? tokenBalanceChanges[baseToken.symbol] * -1 + : tokenBalanceChanges[baseToken.base] + ? tokenBalanceChanges[baseToken.base] * -1 + : 0; + const quoteTokenAmountAdded = tokenBalanceChanges[quoteToken.symbol] + ? tokenBalanceChanges[quoteToken.symbol] * -1 + : tokenBalanceChanges[quoteToken.base] + ? tokenBalanceChanges[quoteToken.base] * -1 + : 0; + signature = signingResponse.transactionHash; + const ammResponse: AMMAddLiquidityResponseType = { + data: { + fee, + baseTokenAmountAdded: baseTokenAmountAdded, + quoteTokenAmountAdded: quoteTokenAmountAdded, + }, + signature: signature, + status: signingResponse.code, + }; + + return [signingResponse, ammResponse]; + } else { + throw new Error(`No AMM pool found for pair ${baseToken.base}-${quoteToken.base}`); + } + } catch (error) { + console.debug(error); + } finally { + this.signingClient.disconnect(); + } + logger.error('Osmosis: Add position failed, reason unknown.'); + } + + /** + * Requires positionId, so needs to be called after OpenPositionCLMM + * + * @param wallet CosmosWallet + * @param req CLMM - AddLiquidityRequestType + */ + async AddLiquidityCLMM( + wallet: CosmosWallet, + req: CLMMAddLiquidityRequestType, + ): Promise<[AddPositionTransactionResponse, CLMMAddLiquidityResponseType]> { + let addLiquidityResponse: CLMMAddLiquidityResponseType = { + data: { + fee: 0, + baseTokenAmountAdded: 0, + quoteTokenAmountAdded: 0, + }, + signature: '', + status: 1, + }; + + let final_poolId; + if (req.positionAddress) { + try { + const allCLPositionsContainer = await this._provider.osmosis.concentratedliquidity.v1beta1.positionById({ + address: req.walletAddress, // endpoint is broken, seems to require positionAddress + positionId: req.positionAddress, + }); + if (allCLPositionsContainer.position.position.address == req.walletAddress) { + // verify this is our position + final_poolId = allCLPositionsContainer.position.position.poolId; + } + } catch (error) { + console.debug(error); + } + } + + let poolsContainer; + try { + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } catch (err) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } + const pools: AnyPoolType[] = poolsContainer.pools; + const prices = {}; //await this.getCoinGeckPricesStored(); + + let filteredPools: ExtendedPool[] = []; + if (final_poolId) { + //@ts-expect-error: Osmosis Case 1 + filteredPools = getPoolByIdAndFilter(this.tokenList, pools, prices, final_poolId, false); + } else { + throw new Error('Osmosis: AddLiquidtyCLMM failed, position not found.'); + } + const pool: ExtendedPool = filteredPools[0]; + + // these two may end up swapped depending on which pool gets selected + let baseToken: CosmosAsset = this.getTokenByBase(pool.token0)!; + let quoteToken: CosmosAsset = this.getTokenByBase(pool.token1)!; + + const gasAdjustment = this.gasAdjustment; + const feeTier = this.feeTier; + + // in case we need to swap these later + let baseTokenAmount = req.baseTokenAmount; + let quoteTokenAmount = req.quoteTokenAmount; + + // set slippage for this to 100 because the pools are too unbalanced + let slippage = 100; + if (req.slippagePct) { + slippage = req.slippagePct; + } else { + slippage = this.getAllowedSlippage(this.allowedSlippage); + } + + try { + const keyWallet = await cWalletMaker(wallet.privkey, 'osmo'); + this.signingClient = await this.osmosisGetSigningStargateClient(this.nodeURL, keyWallet.member); + + if (!this.signingClient || !req.walletAddress) { + logger.error( + "Osmosis: AddLiquidityCLMM failed: Can't instantiate signing client. StargateClient undefined or address undefined.", + ); + throw new Error( + "Osmosis: AddLiquidityCLMM failed: Can't instantiate signing client. StargateClient undefined or address undefined.", + ); + } + + if (!baseTokenAmount && !quoteTokenAmount) { + logger.error('Osmosis: AddLiquidityCLMM failed: Both token amounts equal to 0.'); + throw new Error('Osmosis: AddLiquidityCLMM failed: Both token amounts equal to 0.'); + } + + let baseTokenAmount_bignumber = new BigNumber(0); + let quoteTokenAmount_bignumber = new BigNumber(0); + if (baseTokenAmount) { + baseTokenAmount_bignumber = new BigNumber(baseTokenAmount); + } + if (quoteTokenAmount) { + quoteTokenAmount_bignumber = new BigNumber(quoteTokenAmount); + } + if (baseTokenAmount_bignumber.isEqualTo(0) && quoteTokenAmount_bignumber.isEqualTo(0)) { + logger.error('Osmosis: AddLiquidityCLMM failed: Both token amounts equal to 0.'); + throw new Error('Osmosis: AddLiquidityCLMM failed: Both token amounts equal to 0.'); + } + + // Osmo is weird, we can send in only one type of token to the CL pool, so we need to know which if it's a single token + let singleToken_UseWhich: string | null = null; + if (!baseTokenAmount_bignumber.isEqualTo(0) && quoteTokenAmount_bignumber.isEqualTo(0)) { + singleToken_UseWhich = '0'; + } + if (baseTokenAmount_bignumber.isEqualTo(0) && !quoteTokenAmount_bignumber.isEqualTo(0)) { + singleToken_UseWhich = '1'; + } + // NOT CHECKING (local wallet) BALANCES HERE it will bounce back either way + + let calcedFee; + if (pool) { + // swap token orders to match pool asset orders + if (pool.token0 == baseToken.base && pool.token1 == quoteToken.base) { + [baseToken, quoteToken] = [quoteToken, baseToken]; + [baseTokenAmount, quoteTokenAmount] = [quoteTokenAmount, baseTokenAmount]; + [baseTokenAmount_bignumber, quoteTokenAmount_bignumber] = [ + quoteTokenAmount_bignumber, + baseTokenAmount_bignumber, + ]; + if (singleToken_UseWhich) { + if (singleToken_UseWhich == '0') { + singleToken_UseWhich = '1'; + } else { + singleToken_UseWhich = '0'; + } + } + } + + const gasPrice = await this.getLatestBasePrice(); + const msgs = []; + if (singleToken_UseWhich) { + // in case 1 of the token in amounts == 0, we need to change our Msg up + let singleToken_amount = new BigNumber(0); + let singleToken: CosmosAsset | undefined = undefined; + if (singleToken_UseWhich == '0') { + singleToken_amount = baseTokenAmount_bignumber; + singleToken = baseToken; + } else { + singleToken_amount = quoteTokenAmount_bignumber; + singleToken = quoteToken; + } + + let singleTokenMinAmount; + if (slippage == 100) { + singleTokenMinAmount = '0'; + } else { + singleTokenMinAmount = singleToken_amount + .shiftedBy(this.getExponentByBase(singleToken.base)) + .multipliedBy(100 - slippage) + .dividedBy(100) + .integerValue(BigNumber.ROUND_CEIL); + } + + let MsgAddToPosition; + if (singleToken.base == pool.token0) { + MsgAddToPosition = addToPosition({ + positionId: BigInt(req.positionAddress), + sender: req.walletAddress, + amount0: singleToken_amount.toString(), + amount1: '0', + tokenMinAmount0: singleTokenMinAmount.toString(), + tokenMinAmount1: '0', + }); + } else { + MsgAddToPosition = addToPosition({ + positionId: BigInt(req.positionAddress), + sender: req.walletAddress, + amount0: '0', + amount1: singleToken_amount.toString(), + tokenMinAmount0: '0', + tokenMinAmount1: singleTokenMinAmount.toString(), + }); + } + + msgs.push(MsgAddToPosition); + + const enumFee = FEES.osmosis.joinPool(feeTier); + let gasToUse = enumFee.gas; + try { + const gasEstimation = await this.signingClient.simulate(req.walletAddress, msgs); + gasToUse = gasEstimation; + } catch (error1) { + const error = error1 as Error; + if (error.message.includes('token is lesser than min amount')) { + logger.error( + `Osmosis: AddLiquidityCLMM simulation failed. Amount less than min amount error. tokenMinAmount0: ${singleTokenMinAmount.toString()}.`, + ); + } + } + calcedFee = calculateFee( + Math.round(Number(gasToUse) * (gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + } else { + const baseToken_bignumber = new BigNumber(baseTokenAmount).shiftedBy(this.getExponentByBase(baseToken.base)); + const quoteToken_bignumber = new BigNumber(quoteTokenAmount).shiftedBy( + this.getExponentByBase(quoteToken.base), + ); + + let tokenMinAmount0; + let tokenMinAmount1; + if (slippage == 100) { + tokenMinAmount0 = '0'; + tokenMinAmount1 = '0'; + } else { + tokenMinAmount0 = baseToken_bignumber + .multipliedBy(100 - slippage) + .dividedBy(100) + .integerValue(BigNumber.ROUND_CEIL); + tokenMinAmount1 = quoteToken_bignumber + .multipliedBy(100 - slippage) + .dividedBy(100) + .integerValue(BigNumber.ROUND_CEIL); + } + + let baseTokenAmount_final = baseToken_bignumber.toString(); + let quoteTokenAmount_final = quoteToken_bignumber.toString(); + let tokenMinAmount0_final = tokenMinAmount0.toString(); + let tokenMinAmount1_final = tokenMinAmount1.toString(); + + // alphabetize the coins going in or else invalid coins error (ask me how long it took to debug this completely undocumented issue) + if (!(baseToken.base.toLowerCase() < quoteToken.base.toLowerCase())) { + tokenMinAmount0_final = tokenMinAmount1.toString(); + tokenMinAmount1_final = tokenMinAmount0.toString(); + baseTokenAmount_final = quoteToken_bignumber.toString(); + quoteTokenAmount_final = baseToken_bignumber.toString(); + } + + const msgAddToPosition = addToPosition({ + positionId: BigInt(req.positionAddress), + sender: req.walletAddress, + amount0: baseTokenAmount_final, + amount1: quoteTokenAmount_final, + tokenMinAmount0: tokenMinAmount0_final, + tokenMinAmount1: tokenMinAmount1_final, + }); + + msgs.push(msgAddToPosition); + + const enumFee = FEES.osmosis.joinPool(feeTier); + let gasToUse = enumFee.gas; + try { + const gasEstimation = await this.signingClient.simulate(req.walletAddress, msgs); + gasToUse = gasEstimation; + } catch (error1) { + const error = error1 as Error; + if (error.message.includes('slippage bound')) { + logger.error( + `Osmosis: AddLiquidityCLMM failed: Outside of slippage bounds: insufficient amount of token created. ` + + baseToken.symbol + + `: ` + + baseTokenAmount_final + + ` ` + + quoteToken.symbol + + `: ` + + quoteTokenAmount_final, + ); + } + throw error; + } + + calcedFee = calculateFee( + Math.round(Number(gasToUse) * (gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + } + + if (new BigNumber(calcedFee.gas).gt(new BigNumber(this.gasLimitEstimate))) { + logger.error( + `Osmosis: Gas limit exceeded ${new BigNumber(calcedFee.gas).toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + } + + const signingResponse: AddPositionTransactionResponse = await this.signingClient.signAndBroadcast( + req.walletAddress, + msgs, + calcedFee, + ); + this.signingClient.disconnect(); + let new_position_id = req.positionAddress; + + if (signingResponse?.code !== successfulTransaction) { + addLiquidityResponse = { + data: { + fee: Number(signingResponse.feeAmount), + baseTokenAmountAdded: 0, + quoteTokenAmountAdded: 0, + }, + signature: signingResponse.transactionHash, + status: signingResponse.code, + }; + return [signingResponse, addLiquidityResponse]; + } + + let tokenBalanceChanges: Record = {}; + { + const dissectRes = (await this.dissectTransactionResponse(wallet.address, signingResponse)) as [ + Record, + Record, + string, + ]; + tokenBalanceChanges = dissectRes[1]; + new_position_id = dissectRes[2]; + } + + const baseTokenAmountAdded = tokenBalanceChanges[baseToken.symbol] + ? tokenBalanceChanges[baseToken.symbol] * -1 + : tokenBalanceChanges[baseToken.base] + ? tokenBalanceChanges[baseToken.base] * -1 + : 0; + const quoteTokenAmountAdded = tokenBalanceChanges[quoteToken.symbol] + ? tokenBalanceChanges[quoteToken.symbol] * -1 + : tokenBalanceChanges[quoteToken.base] + ? tokenBalanceChanges[quoteToken.base] * -1 + : 0; + addLiquidityResponse = { + data: { + fee: Number(signingResponse.feeAmount), + baseTokenAmountAdded: baseTokenAmountAdded, + quoteTokenAmountAdded: quoteTokenAmountAdded, + newPositionAddress: new_position_id, + }, + signature: signingResponse.transactionHash, + status: signingResponse.code, + }; + + return [signingResponse, addLiquidityResponse]; + } + } catch (error) { + console.debug(error); + logger.error('Osmosis: AddLiquidityCLMM failed, reason unknown.'); + throw error; + } finally { + this.signingClient.disconnect(); + } + logger.error('Osmosis: AddLiquidityCLMM failed, reason unknown.'); + throw new Error('Osmosis: AddLiquidityCLMM, reason unknown.'); + } + + /** + * Requires poolAddress, so called after fetchPools() + * + * @param wallet CosmosWallet + * @param req CLMM - OpenPositionRequestType + */ + async OpenPositionCLMM( + wallet: CosmosWallet, + req: CLMMOpenPositionRequestType, + ): Promise<[AddPositionTransactionResponse, CLMMOpenPositionResponseType]> { + let openPositionResponse: CLMMOpenPositionResponseType = { + signature: '', + status: 1, + data: { fee: 0, baseTokenAmountAdded: 0, quoteTokenAmountAdded: 0, positionAddress: '', positionRent: 0 }, + }; + const gasAdjustment = this.gasAdjustment; + const feeTier = this.feeTier; + + // set slippage for this to 100 because the pools are too unbalanced + let slippage = 100; + if (req.slippagePct) { + slippage = req.slippagePct; + } else { + slippage = this.getAllowedSlippage(this.allowedSlippage); + } + + try { + const keyWallet = await cWalletMaker(wallet.privkey, 'osmo'); + this.signingClient = await this.osmosisGetSigningStargateClient(this.nodeURL, keyWallet.member); + + if (!this.signingClient || !req.walletAddress) { + logger.error( + "Osmosis: OpenPoolPosition failed: Can't instantiate signing client. StargateClient undefined or address undefined.", + ); + throw new Error( + "Osmosis: OpenPoolPosition failed: Can't instantiate signing client. StargateClient undefined or address undefined.", + ); + } + let poolsContainer; + try { + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } catch (err) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } + const pools: AnyPoolType[] = poolsContainer.pools; + const prices = await this.getCoinGeckPricesStored(); + + //@ts-expect-error: Osmosis Case 1 + const pool = getPoolByAddressAndFilter(this.tokenList, pools, prices, req.poolAddress, false)[0]; + // in case we need to swap these later + // these two may end up swapped depending on which pool gets selected (tokens must be added in correct order via rpc) + const baseToken: CosmosAsset = this.getTokenByBase(pool.token0)!; + const quoteToken: CosmosAsset = this.getTokenByBase(pool.token1)!; + const baseTokenAmount = req.baseTokenAmount; + const quoteTokenAmount = req.quoteTokenAmount; + + if (!baseTokenAmount && !quoteTokenAmount) { + logger.error('Osmosis: OpenPoolPosition failed: Both token amounts equal to 0.'); + throw new Error('Osmosis: OpenPoolPosition failed: Both token amounts equal to 0.'); + } + + let baseTokenAmount_bignumber = new BigNumber(0); + let quoteTokenAmount_bignumber = new BigNumber(0); + if (baseTokenAmount) { + baseTokenAmount_bignumber = new BigNumber(baseTokenAmount); + } + if (quoteTokenAmount) { + quoteTokenAmount_bignumber = new BigNumber(quoteTokenAmount); + } + if (baseTokenAmount_bignumber.isEqualTo(0) && quoteTokenAmount_bignumber.isEqualTo(0)) { + logger.error('Osmosis: OpenPoolPosition failed: Both token amounts equal to 0.'); + throw new Error('Osmosis: OpenPoolPosition failed: Both token amounts equal to 0.'); + } + + // Osmo is weird, we can send in only one type of token to the CL pool, so we need to know which if it's a single token + let singleToken_UseWhich: string | null = null; + if (!baseTokenAmount_bignumber.isEqualTo(0) && quoteTokenAmount_bignumber.isEqualTo(0)) { + singleToken_UseWhich = '0'; + } + if (baseTokenAmount_bignumber.isEqualTo(0) && !quoteTokenAmount_bignumber.isEqualTo(0)) { + singleToken_UseWhich = '1'; + } + // NOT CHECKING (local wallet) BALANCES HERE it will bounce back either way + + let calcedFee; + let singleTokenMinAmount = new BigNumber(0); + if (pool) { + const gasPrice = await this.getLatestBasePrice(); + const msgs = []; + if (singleToken_UseWhich) { + // in case 1 of the amounts == 0 + let singleToken_amount = new BigNumber(0); + let singleToken: CosmosAsset | undefined = undefined; + if (singleToken_UseWhich == '0') { + singleToken_amount = baseTokenAmount_bignumber; + singleToken = baseToken; + } else { + singleToken_amount = quoteTokenAmount_bignumber; + singleToken = quoteToken; + } + const inputCoin = { + denom: singleToken.base, + amount: singleToken_amount.shiftedBy(this.getExponentByBase(singleToken.base)).toString(), + }; + + if (slippage != 100) { + singleTokenMinAmount = singleToken_amount + .shiftedBy(this.getExponentByBase(singleToken.base)) + .multipliedBy(100 - slippage) + .dividedBy(100) + .integerValue(BigNumber.ROUND_CEIL); + } + + const lowerTick = calculatePriceToTick( + req.lowerPrice.toString(), + Number(pool.exponentAtPriceOne.toString()), + Number(pool.tickSpacing.toString()), + true, + ); + const upperTick = calculatePriceToTick( + req.upperPrice.toString(), + Number(pool.exponentAtPriceOne.toString()), + Number(pool.tickSpacing.toString()), + false, + ); + + let MsgCreatePosition; + if (singleToken.base == pool.token0) { + MsgCreatePosition = createPosition({ + // @ts-expect-error: bad osmojs models + poolId: pool.id.toString(), + sender: req.walletAddress, + // @ts-expect-error: bad osmojs models + lowerTick: lowerTick, + // @ts-expect-error: bad osmojs models + upperTick: upperTick, + tokensProvided: [inputCoin], + tokenMinAmount0: singleTokenMinAmount.toString(), + tokenMinAmount1: '0', + }); + } else { + MsgCreatePosition = createPosition({ + // @ts-expect-error: bad osmojs models + poolId: pool.id.toString(), + sender: req.walletAddress, + // @ts-expect-error: bad osmojs models + lowerTick: lowerTick, + // @ts-expect-error: bad osmojs models + upperTick: upperTick, + tokensProvided: [inputCoin], + tokenMinAmount0: '0', + tokenMinAmount1: singleTokenMinAmount.toString(), + }); + } + + msgs.push(MsgCreatePosition); + + const enumFee = FEES.osmosis.joinPool(feeTier); + let gasToUse = enumFee.gas; + try { + const gasEstimation = await this.signingClient.simulate(req.walletAddress, msgs); + gasToUse = gasEstimation; + } catch (error1) { + const error = error1 as Error; + if (error.message.includes('token is lesser than min amount')) { + logger.error( + `Osmosis: OpenPoolPosition simulation failed. Amount less than min amount error. tokenMinAmount0: ${singleTokenMinAmount.toString()}.`, + ); + throw new Error( + `Osmosis: OpenPoolPosition simulation failed. Amount less than min amount error. tokenMinAmount0: ${singleTokenMinAmount.toString()}.`, + ); + } else if (error.message.includes('Not providing enough liquidity in token')) { + logger.error( + `Osmosis: OpenPoolPosition simulation failed. Single token provided and failed to translate amount to positive liquidity. The given tick range is inactive. If the given range becomes activated, two tokens will be needed as opposed to one.`, + ); + throw new Error( + `Osmosis: OpenPoolPosition simulation failed. Single token provided and failed to translate amount to positive liquidity. The given tick range is inactive. If the given range becomes activated, two tokens will be needed as opposed to one.`, + ); + } else { + logger.error(`Osmosis: Simulate failed.`); + logger.error(error); + throw error; + } + } + calcedFee = calculateFee( + Math.round(Number(gasToUse) * (gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + } else { + const allCoins = []; + allCoins.push({ + denom: baseToken.base, + amount: new BigNumber(baseTokenAmount).shiftedBy(this.getExponentByBase(baseToken.base)).toString(), + }); + allCoins.push({ + denom: quoteToken.base, + amount: new BigNumber(quoteTokenAmount).shiftedBy(this.getExponentByBase(quoteToken.base)).toString(), + }); + + const baseToken_bignumber = new BigNumber(baseTokenAmount); + const quoteToken_bignumber = new BigNumber(quoteTokenAmount); + + let tokenMinAmount0; + let tokenMinAmount1; + if (slippage == 100) { + tokenMinAmount0 = '0'; + tokenMinAmount1 = '0'; + } else { + tokenMinAmount0 = baseToken_bignumber + .shiftedBy(this.getExponentByBase(baseToken.base)) + .multipliedBy(100 - slippage) + .dividedBy(100) + .integerValue(BigNumber.ROUND_CEIL); + tokenMinAmount1 = quoteToken_bignumber + .shiftedBy(this.getExponentByBase(quoteToken.base)) + .multipliedBy(100 - slippage) + .dividedBy(100) + .integerValue(BigNumber.ROUND_CEIL); + } + + const lowerTick = findTickForPrice( + req.lowerPrice.toString(), + Number(pool.exponentAtPriceOne.toString()), + Number(pool.tickSpacing.toString()), + true, + ); // pool.currentTick, + const upperTick = findTickForPrice( + req.upperPrice.toString(), + Number(pool.exponentAtPriceOne.toString()), + Number(pool.tickSpacing.toString()), + false, + ); + + const tokenMinAmount0_final = tokenMinAmount0.toString(); + const tokenMinAmount1_final = tokenMinAmount1.toString(); + + // alphabetize the coins going in or else invalid coins error (ask me how long it took to debug this completely undocumented issue) + if (!(baseToken.base.toLowerCase() < quoteToken.base.toLowerCase())) { + allCoins.reverse(); + } + + const MsgCreatePosition = createPosition({ + // @ts-expect-error: bad osmojs models + poolId: pool.id.toString(), + sender: req.walletAddress, + // @ts-expect-error: bad osmojs models + lowerTick: lowerTick, + // @ts-expect-error: bad osmojs models + upperTick: upperTick, + tokensProvided: allCoins, + tokenMinAmount0: tokenMinAmount0_final, + tokenMinAmount1: tokenMinAmount1_final, + }); + + msgs.push(MsgCreatePosition); + + const enumFee = FEES.osmosis.joinPool(feeTier); + let gasToUse = enumFee.gas; + try { + const gasEstimation = await this.signingClient.simulate(req.walletAddress, msgs); + gasToUse = gasEstimation; + } catch (error1) { + const error = error1 as Error; + if (error.message.includes('token is lesser than min amount')) { + logger.error( + `Osmosis: OpenPoolPosition simulation failed. Amount less than min amount error. tokenMinAmount0: ${singleTokenMinAmount.toString()}.` + + error.message, + ); + throw new Error( + `Osmosis: OpenPoolPosition simulation failed. Amount less than min amount error. tokenMinAmount0: ${singleTokenMinAmount.toString()}.` + + error.message, + ); + } else if (error.message.includes('Not providing enough liquidity in token')) { + logger.error( + `Osmosis: OpenPoolPosition simulation failed. Single token provided and failed to translate amount to positive liquidity. The given tick range is inactive. If the given range becomes activated, two tokens will be needed as opposed to one.` + + error.message, + ); + throw new Error( + `Osmosis: OpenPoolPosition simulation failed. Single token provided and failed to translate amount to positive liquidity. The given tick range is inactive. If the given range becomes activated, two tokens will be needed as opposed to one.` + + error.message, + ); + } else if (error.message.includes('slippage bound: insufficient amount of token ')) { + logger.error( + `Osmosis: OpenPoolPosition simulation failed. Insufficient amount of token provided.` + error.message, + ); + throw new Error( + `Osmosis: OpenPoolPosition simulation failed. Insufficient amount of token provided.` + error.message, + ); + } else { + logger.error(`Osmosis: Simulate failed.`); + logger.error(error); + throw error; + } + } + + calcedFee = calculateFee( + Math.round(Number(gasToUse) * (gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + } + + if (new BigNumber(calcedFee.gas).gt(new BigNumber(this.gasLimitEstimate))) { + logger.error( + `Osmosis: Gas limit exceeded ${new BigNumber(calcedFee.gas).toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + } + + let signingResponse: AddPositionTransactionResponse; + try { + signingResponse = await this.signingClient.signAndBroadcast(req.walletAddress, msgs, calcedFee); + this.signingClient.disconnect(); + } catch (error1) { + const error = error1 as Error; + if (error.message.includes('token is lesser than min amount')) { + logger.error( + `Osmosis: OpenPoolPosition failed. Amount less than min amount error. tokenMinAmount0: ${singleTokenMinAmount.toString()}.`, + ); + throw new Error( + `Osmosis: OpenPoolPosition failed. Amount less than min amount error. tokenMinAmount0: ${singleTokenMinAmount.toString()}.`, + ); + } else if (error.message.includes('Not providing enough liquidity in token')) { + logger.error( + `Osmosis: OpenPoolPosition failed. Single token provided and failed to translate amount to positive liquidity. The given tick range is inactive. If the given range becomes activated, two tokens will be needed as opposed to one.`, + ); + throw new Error( + `Osmosis: OpenPoolPosition failed. Single token provided and failed to translate amount to positive liquidity. The given tick range is inactive. If the given range becomes activated, two tokens will be needed as opposed to one.`, + ); + } else { + logger.error(`Osmosis: Sign and Broadcast failed.`); + logger.error(error); + throw error; + } + } + + if (signingResponse?.code !== successfulTransaction) { + openPositionResponse = { + signature: signingResponse.transactionHash, + status: signingResponse.code, + }; + return [signingResponse, openPositionResponse]; + } + + let tokenBalanceChanges: Record = {}; + let position_id = ''; + { + const dissectRes = (await this.dissectTransactionResponse(wallet.address, signingResponse)) as [ + Record, + Record, + string, + ]; + tokenBalanceChanges = dissectRes[1]; + position_id = dissectRes[2]; + } + + const baseTokenAmountAdded = tokenBalanceChanges[baseToken.symbol] + ? tokenBalanceChanges[baseToken.symbol] * -1 + : tokenBalanceChanges[baseToken.base] + ? tokenBalanceChanges[baseToken.base] * -1 + : 0; + const quoteTokenAmountAdded = tokenBalanceChanges[quoteToken.symbol] + ? tokenBalanceChanges[quoteToken.symbol] * -1 + : tokenBalanceChanges[quoteToken.base] + ? tokenBalanceChanges[quoteToken.base] * -1 + : 0; + openPositionResponse = { + signature: signingResponse.transactionHash, + status: signingResponse.code, + data: { + fee: signingResponse.feeAmount ? Number(signingResponse.feeAmount) : 0, + baseTokenAmountAdded: baseTokenAmountAdded, + quoteTokenAmountAdded: quoteTokenAmountAdded, + positionAddress: position_id, + positionRent: 0, + }, + }; + return [signingResponse, openPositionResponse]; + } + } catch (error) { + console.debug(error); + } finally { + this.signingClient.disconnect(); + } + logger.error('Osmosis: Open Pool Position failed, reason unknown.'); + throw new Error('Osmosis: Open Pool Position, reason unknown.'); + } + + /** + * Stub. No position sim supported on Osmosis. + * + * @param wallet CosmosWallet + * @param req CLMM - OpenPositionRequestType + */ + async QuotePositionCLMM(req: CLMMOpenPositionRequestType): Promise { + try { + const quotePositionResponse: QuotePositionResponseType = { + baseLimited: false, + baseTokenAmount: req.baseTokenAmount, + quoteTokenAmount: req.quoteTokenAmount, + baseTokenAmountMax: req.baseTokenAmount, + quoteTokenAmountMax: req.quoteTokenAmount, + liquidity: 0, + }; + return quotePositionResponse; + } catch (error) { + console.debug(error); + } + logger.error('Osmosis: Quote Position failed, reason unknown.'); + } + + /** + * Exchange pool liquidity shares for amounts of tokens from a pool + * + * @param wallet CosmosWallet + * @param req AMM - RemoveLiquidityRequestType + * @param feeTier_input? high/medium/low + * @param gasAdjustment_input? extra gas as number, eg. 1.2 + */ + async removeLiquidityAMM( + wallet: CosmosWallet, + req: AMMRemoveLiquidityRequestType, + feeTier_input?: string, + gasAdjustment_input?: number, + ): Promise<[ReduceLiquidityTransactionResponse, AMMRemoveLiquidityResponseType]> { + let response_signature = ''; + let response_fee = 0; + let ammResponse: AMMRemoveLiquidityResponseType = { + signature: response_signature, + status: 1, + }; + + if (!req.poolAddress) { + throw new Error('Osmosis: reducePositionAMM Missing poolAddress or token pair'); + } + + const poolAddress = req.poolAddress; + let baseToken: CosmosAsset; + let quoteToken: CosmosAsset; + + const address = req.walletAddress; + const percent = req.percentageToRemove; // total percent of pool shares + // new models not sending in allowed slippage for remove so using default I guess + const slippage = this.getAllowedSlippage(this.allowedSlippage); + + let feeTier = this.feeTier; + if (feeTier_input) { + feeTier = feeTier_input; + } + let gasAdjustment = this.gasAdjustment; + if (gasAdjustment_input) { + gasAdjustment = gasAdjustment_input; + } + + try { + const keyWallet = await cWalletMaker(wallet.privkey, 'osmo'); + this.signingClient = await this.osmosisGetSigningStargateClient(this.nodeURL, keyWallet.member); + + const balancesContainer = await this._provider.cosmos.bank.v1beta1.allBalances({ + address: address, + pagination: { + key: new Uint8Array(), + offset: BigInt(0), + limit: BigInt(10000), + countTotal: false, + reverse: false, + }, + }); + const balances = balancesContainer.balances; + const lockedCoinsContainer = await this._provider.osmosis.lockup.accountLockedCoins({ + owner: address, + }); + const lockedCoins: Coin[] = lockedCoinsContainer.lockedCoins ? lockedCoinsContainer.lockedCoins : []; + + // RETURN TYPES: + // concentrated-liquidity/pool || cosmwasmpool/v1beta1/model/pool || gamm/pool-models/balancer/balancerPool || gamm/pool-models/stableswap/stableswap_pool + let poolsContainer; + try { + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } catch (err) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } + const pools: ExtendedPool[] = poolsContainer.pools; + const fees = await parseFees(pools); + const prices = await this.getCoinGeckPricesStored(); + + //@ts-expect-error: Osmosis Case 1 + const filteredPools = getPoolByAddressAndFilter(this.tokenList, pools, prices, poolAddress, false); // removes stableswap, !token.denom.startsWith('gamm/pool'), has price, has osmosisAsset + const extendedPools = filteredPools.map((pool) => + extendPool(this.assetList, { pool, fees, balances, lockedCoins, prices: prices }), + ); + + let currentPool: ExtendedPool; + const final_poolAddress = req.poolAddress; + if (final_poolAddress) { + currentPool = extendedPools.find((pl) => pl.address == final_poolAddress); + } else { + currentPool = extendedPools.find((pl) => pl.myLiquidity && pl.myLiquidity != '0'); // first one we find we have coins in + } + + if (!currentPool || !currentPool.myLiquidity || currentPool.myLiquidity == '0') { + throw new Error('Osmosis: No liquidity found for poolAddress or token pair.'); + } + + if (!baseToken) { + baseToken = this.getTokenByBase(currentPool.poolAssets[0].token.denom)!; + } + if (!quoteToken) { + quoteToken = this.getTokenByBase(currentPool.poolAssets[1].token.denom)!; + } + + let tokenOutMins: Coin[] = []; + const msgs = []; + let myLiquidityDollarValue; + if (currentPool.myLiquidityDollarValue) { + myLiquidityDollarValue = new BigNumber(currentPool.myLiquidityDollarValue || 0) + .multipliedBy(percent) + .div(100) + .toString(); + } + + const unbondedShares = convertDollarValueToShares( + //@ts-expect-error Case 2 + this.assetList, + myLiquidityDollarValue || 0, + currentPool, + prices, + ); + + const myCoins = convertDollarValueToCoins( + //@ts-expect-error: Osmosis Case 2 + this.assetList, + myLiquidityDollarValue || 0, + currentPool, + prices, + ); + + const coinsNeeded: Coin[] = []; + myCoins.forEach(({ denom, amount }) => { + const amountWithSlippage = new BigNumber(amount).multipliedBy(new BigNumber(100).minus(slippage)).div(100); + if (amountWithSlippage.isGreaterThanOrEqualTo(1)) { + coinsNeeded.push({ + denom, + amount: this.noDecimals(amountWithSlippage.toString()), + }); + } + }); + + const shareInAmount = new BigNumber(unbondedShares).shiftedBy(18).decimalPlaces(0).toString(); + + // alphabetize the coins going in or else invalid coins error + if (coinsNeeded.length > 1) { + if (!(coinsNeeded[0].denom.toLowerCase() < coinsNeeded[1].denom.toLowerCase())) { + coinsNeeded.reverse(); + } + } + + tokenOutMins = coinsNeeded.map((c: Coin) => { + return coin(c.amount, c.denom); + }); + + if (slippage == 100) { + tokenOutMins = []; + } + + const msg = exitPool({ + // @ts-expect-error: bad osmojs models + poolId: currentPool.id.toString(), + sender: address, + shareInAmount, + tokenOutMins: tokenOutMins, + }); + msgs.push(msg); + + const enumFee = FEES.osmosis.exitPool(feeTier); + let gasToUse = enumFee.gas; + try { + const gasEstimation = await this.signingClient.simulate(address, msgs); + gasToUse = gasEstimation; + } catch (error1) { + const error = error1 as Error; + if (error.message.includes('token is lesser than min amount')) { + let composeTokenOutMins = ''; + for (let idx = 0; idx < tokenOutMins.length; idx++) { + composeTokenOutMins += ' denom: ' + tokenOutMins[idx].denom + ' amount: ' + tokenOutMins[idx].amount; + } + logger.error( + `Osmosis: ReducePosition failed: Amount less than min amount error. tokenOutMins: ${composeTokenOutMins}`, + ); + throw new Error( + `Osmosis: ReducePosition failed: Amount less than min amount error. tokenOutMins: ${composeTokenOutMins}`, + ); + } else { + logger.error(`Osmosis: Simulate failed.`); + logger.error(error); + throw error; + } + } + + const gasPrice = await this.getLatestBasePrice(); + const calcedFee = calculateFee( + Math.round(Number(gasToUse) * (gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + + if (new BigNumber(calcedFee.gas).gt(new BigNumber(this.gasLimitEstimate))) { + logger.error( + `Osmosis: Gas limit exceeded ${new BigNumber(calcedFee.gas).toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + throw new Error( + `Osmosis: Gas limit exceeded ${new BigNumber(calcedFee.gas).toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + } + + const signingResponse: ReduceLiquidityTransactionResponse = await this.signingClient.signAndBroadcast( + address, + msgs, + calcedFee, + ); + this.signingClient.disconnect(); + + if (signingResponse?.code !== successfulTransaction) { + signingResponse.balances = []; + return [signingResponse, ammResponse]; + } + + let tokenBalanceChanges: Record = {}; + { + const dissectRes = (await this.dissectTransactionResponse(wallet.address, signingResponse)) as [ + Record, + Record, + string, + ]; + tokenBalanceChanges = dissectRes[1]; + } + + const baseTokenAmountRemoved = tokenBalanceChanges[baseToken.symbol] + ? tokenBalanceChanges[baseToken.symbol] + : tokenBalanceChanges[baseToken.base] + ? tokenBalanceChanges[baseToken.base] + : 0; + const quoteTokenAmountRemoved = tokenBalanceChanges[quoteToken.symbol] + ? tokenBalanceChanges[quoteToken.symbol] + : tokenBalanceChanges[quoteToken.base] + ? tokenBalanceChanges[quoteToken.base] + : 0; + response_signature = signingResponse.transactionHash; + response_fee = signingResponse.feeAmount ? Number(signingResponse.feeAmount) : 0; + ammResponse = { + signature: response_signature, + status: signingResponse.code, + data: { + fee: response_fee, + baseTokenAmountRemoved: baseTokenAmountRemoved, + quoteTokenAmountRemoved: quoteTokenAmountRemoved, + }, + }; + + return [signingResponse, ammResponse]; + } catch (error) { + console.debug(error); + } finally { + this.signingClient.disconnect(); + } + logger.error('Osmosis: ReducePosition failed, reason unknown.'); + } + + /** + * exchange pool liquidity shares for amounts of tokens from a pool + * + * @param wallet CosmosWallet + * @param req CLMM - RemoveLiquidityRequestType + */ + async removeLiquidityCLMM( + wallet: CosmosWallet, + req: CLMMRemoveLiquidityRequestType, + ): Promise { + let clPosition; + let final_poolId; + try { + const clPositionsContainer = await this._provider.osmosis.concentratedliquidity.v1beta1.positionById({ + address: req.walletAddress, // doesn't actually check the address + positionId: req.positionAddress, + }); + if (clPositionsContainer.position.position.address == req.walletAddress) { + // is this our position? + final_poolId = clPositionsContainer.position.position.poolId; + clPosition = clPositionsContainer.position; + } + } catch (error) { + console.debug(error); + throw new Error('Osmosis: RemoveLiquidityCLMM failed, position not found.'); + } + + let poolsContainer; + try { + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } catch (err) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } + const pools: AnyPoolType[] = poolsContainer.pools; + const prices = await this.getCoinGeckPricesStored(); + let filteredPools: ExtendedPool[] = []; + if (final_poolId) { + //@ts-expect-error: Osmosis Case 1 + filteredPools = getPoolByIdAndFilter(this.tokenList, pools, prices, final_poolId, false); + } else { + throw new Error('Osmosis: AddLiquidtyCLMM failed, position not found.'); + } + const pool: ExtendedPool = filteredPools[0]; + const baseToken: CosmosAsset = this.getTokenByBase(pool.token0)!; + const quoteToken: CosmosAsset = this.getTokenByBase(pool.token1)!; + + // new models not sending in allowed slippage for remove so using default I guess + const feeTier = this.feeTier; + + try { + const keyWallet = await cWalletMaker(wallet.privkey, 'osmo'); + this.signingClient = await this.osmosisGetSigningStargateClient(this.nodeURL, keyWallet.member); + + const percent = req.percentageToRemove; // total percent of pool shares + + const tokenOutMins: Coin[] = []; + const msgs = []; + let myLiquidityToRemove; + myLiquidityToRemove = new BigNumber(clPosition.position.liquidity || 0) + .multipliedBy(percent) + .div(100) + .decimalPlaces(18); + + if (new BigNumber(clPosition.position.liquidity).lt(myLiquidityToRemove)) { + myLiquidityToRemove = new BigNumber(clPosition.position.liquidity).decimalPlaces(18); + } + + const msgWithdrawPosition = withdrawPosition({ + sender: req.walletAddress, + positionId: BigInt(req.positionAddress), + liquidityAmount: myLiquidityToRemove.toString(), + }); + msgs.push(msgWithdrawPosition); + + const enumFee = FEES.osmosis.exitPool(feeTier); + let gasToUse = enumFee.gas; + try { + const gasEstimation = await this.signingClient.simulate(req.walletAddress, msgs); + gasToUse = gasEstimation; + } catch (error1) { + const error = error1 as Error; + if (error.message.includes('token is lesser than min amount')) { + let composeTokenOutMins = ''; + for (let idx = 0; idx < tokenOutMins.length; idx++) { + composeTokenOutMins += ' denom: ' + tokenOutMins[idx].denom + ' amount: ' + tokenOutMins[idx].amount; + } + logger.error( + `Osmosis: ReducePosition failed: Amount less than min amount error. tokenOutMins: ${composeTokenOutMins}`, + ); + } + } + + const gasPrice = await this.getLatestBasePrice(); + const calcedFee = calculateFee( + Math.round(Number(gasToUse) * (this.gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + + if (new BigNumber(calcedFee.gas).gt(new BigNumber(this.gasLimitEstimate))) { + logger.error( + `Osmosis: Gas limit exceeded ${new BigNumber(calcedFee.gas).toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`, + ); + } + + const signingResponse: ReduceLiquidityTransactionResponse = await this.signingClient.signAndBroadcast( + req.walletAddress, + msgs, + calcedFee, + ); + this.signingClient.disconnect(); + + // const new_position_id = req.positionAddress; // this doesn't actually change on removeLiquidityCL, only addLiquidityCL + if (signingResponse?.code !== successfulTransaction) { + signingResponse.balances = []; + const finalResponse: CLMMRemoveLiquidityResponseType = { + signature: signingResponse.transactionHash, + status: signingResponse.code, + }; + return finalResponse; + } + + logger.info( + `Osmosis: Liquidity removed, txHash is ${signingResponse.transactionHash}, gasUsed is ${signingResponse.gasUsed}.`, + ); + + let tokenBalanceChanges: Record = {}; + { + const dissectRes = (await this.dissectTransactionResponse(wallet.address, signingResponse)) as [ + Record, + Record, + string, + ]; + tokenBalanceChanges = dissectRes[1]; + } + + const response_fee = signingResponse.feeAmount ? Number(signingResponse.feeAmount) : 0; + + const finalResponse: CLMMRemoveLiquidityResponseType = { + signature: signingResponse.transactionHash, + status: signingResponse.code, + data: { + fee: response_fee, + baseTokenAmountRemoved: tokenBalanceChanges[baseToken.symbol] ? tokenBalanceChanges[baseToken.symbol] : 0, + quoteTokenAmountRemoved: tokenBalanceChanges[quoteToken.symbol] ? tokenBalanceChanges[quoteToken.symbol] : 0, + }, + }; + return finalResponse; + } catch (error) { + console.debug(error); + } finally { + this.signingClient.disconnect(); + } + logger.error('Osmosis: ReducePosition failed, reason unknown.'); + } + + /** + * Collects rewards on CL pool + * + * @param wallet CosmosWallet + * @param req CLMM - CollectFeesRequestType + */ + async collectRewardsIncentives( + wallet: CosmosWallet, + req: CLMMCollectFeesRequestType, + ): Promise { + try { + const keyWallet = await cWalletMaker(wallet.privkey, 'osmo'); + this.signingClient = await this.osmosisGetSigningStargateClient(this.nodeURL, keyWallet.member); + + let final_poolId = undefined; + try { + const allCLPositionsContainer = await this._provider.osmosis.concentratedliquidity.v1beta1.positionById({ + address: req.walletAddress, // doesn't actually check the address + positionId: req.positionAddress, + }); + if (allCLPositionsContainer.position.position.address == req.walletAddress) { + final_poolId = allCLPositionsContainer.position.position.poolId; + } + } catch (error) { + console.debug(error); + throw new Error('Osmosis: Collect fees failed to find position: ' + error); + } + + let poolsContainer; + try { + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } catch (err) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } + const pools: ExtendedPool[] = poolsContainer.pools; + const prices = await this.getCoinGeckPricesStored(); + + //@ts-expect-error: Osmosis Case 1 + const filteredPools = getPoolByIdAndFilter(this.tokenList, pools, prices, BigInt(final_poolId), false); + const pool = filteredPools.filter((pl) => pl.id.toString() == final_poolId!.toString())[0]; + const baseToken: CosmosAsset = this.getTokenByBase(pool.token0)!; + const quoteToken: CosmosAsset = this.getTokenByBase(pool.token1)!; + + const msgs = []; + const positionIds: string[] = [req.positionAddress.toString()]; + const msg1 = collectSpreadRewards({ + // @ts-expect-error: bad osmojs models + positionIds: positionIds, + sender: req.walletAddress, + }); + const msg2 = collectIncentives({ + // @ts-expect-error: bad osmojs models + positionIds: positionIds, + sender: req.walletAddress, + }); + msgs.push(msg1); + msgs.push(msg2); + + const enumFee = FEES.osmosis.exitPool(this.feeTier); + let gasToUse = enumFee.gas; + const gasPrice = await this.getLatestBasePrice(); + try { + const gasEstimation = await this.signingClient.simulate(req.walletAddress, msgs); + gasToUse = gasEstimation; + } catch (error) { + console.debug(error); + } + const calcedFee = calculateFee( + Math.round(Number(gasToUse) * (this.gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + + const signingResponse: ReduceLiquidityTransactionResponse | any = await this.signingClient.signAndBroadcast( + req.walletAddress, + msgs, + calcedFee, + ); + this.signingClient.disconnect(); + + if (signingResponse?.code !== successfulTransaction) { + signingResponse.balances = []; + const response: CLMMCollectFeesResponseType = { + signature: signingResponse.transactionHash, + status: signingResponse.code, + }; + return response; + } + + let tokenBalanceChanges: Record = {}; + { + const dissectRes = (await this.dissectTransactionResponse(wallet.address, signingResponse)) as [ + Record, + Record, + string, + ]; + tokenBalanceChanges = dissectRes[1]; + } + + logger.info( + `Osmosis: Collected Fees, txHash is ${signingResponse.transactionHash}, gasUsed is ${signingResponse.gasUsed}.`, + ); + const response: CLMMCollectFeesResponseType = { + signature: signingResponse.transactionHash, + status: 0, + data: { + fee: signingResponse.feeAmount ? signingResponse.feeAmount : 0, + baseFeeAmountCollected: tokenBalanceChanges[baseToken.symbol] ? tokenBalanceChanges[baseToken.symbol] : 0, + quoteFeeAmountCollected: tokenBalanceChanges[quoteToken.symbol] ? tokenBalanceChanges[quoteToken.symbol] : 0, + }, + }; + return response; + } catch (error) { + console.debug(error); + } + logger.error('Osmosis: CollectRewardsIncentives failed, reason unknown.'); + throw new Error('Osmosis: CollectRewardsIncentives failed, reason unknown.'); + } + + /** + * Returns all pools and their prices for pool address or by token (not checking wallet balances currently) + * + * @param poolType amm || clmm + * @param token0_in Requires both tokens or pool address + * @param token1_in + * @param poolAddress May be required + */ + async findPoolsPrices( + poolType: string, + poolAddress?: string, + token0_in?: CosmosAsset, + token1_in?: CosmosAsset, + ): Promise { + try { + let token0: CosmosAsset = token0_in; + let token1: CosmosAsset = token1_in; + + const balances: Coin[] = []; + const lockedCoins: Coin[] = []; + + // RETURN TYPES: + // concentrated-liquidity/pool || cosmwasmpool/v1beta1/model/pool || gamm/pool-models/balancer/balancerPool || gamm/pool-models/stableswap/stableswap_pool + let poolsContainer; + try { + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } catch (err) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } + + const pools: ExtendedPool[] = poolsContainer.pools; + const prices = await this.getCoinGeckPricesStored(); + const fees = await parseFees(pools); + let filteredPools: ExtendedPool[] = []; + + if (poolAddress) { + //@ts-expect-error: Osmosis Case 1 + filteredPools = getPoolByAddressAndFilter(this.tokenList, pools, prices, poolAddress, true); + if (filteredPools && filteredPools.length == 1) { + if (filteredPools[0].token0) { + // CL pool + token0 = this.getTokenByBase(filteredPools[0].token0); + token1 = this.getTokenByBase(filteredPools[0].token1); + } else { + if (filteredPools[0].poolAssets && filteredPools[0].poolAssets.length > 0) { + token0 = this.getTokenByBase(filteredPools[0].poolAssets[0].token.denom); + token1 = this.getTokenByBase(filteredPools[0].poolAssets[1].token.denom); + } + } + } + } else { + if (poolType == 'amm') { + //@ts-expect-error: Osmosis Case 1 + filteredPools = filterPoolsGAMM(this.tokenList, pools, prices); + } else if ((poolType = 'clmm')) { + //@ts-expect-error: Osmosis Case 1 + filteredPools = filterPoolsCLMM(this.tokenList, pools, prices); + } + } + if (!filteredPools || filteredPools.length == 0) { + logger.error('Osmosis: Failed to find pool for address.'); + return { pools: [], prices: [] }; + } + if (!token0 || !token1) { + logger.error('Osmosis: Failed to find tokens for pool address.'); + return { pools: [], prices: [] }; + } + const exponentToken0 = this.getExponentByBase(token0.base); + const exponentToken1 = this.getExponentByBase(token1.base); + const extendedPools = filteredPools.map((pool) => + extendPool(this.assetList, { pool, fees, balances, lockedCoins, prices: prices }), + ); + + const pricesOut: string[] = []; + const returnPools: SerializableExtendedPool[] = []; + extendedPools.forEach(function (cPool) { + let foundToken0 = false; + let foundToken1 = false; + if (cPool.token0) { + if (cPool.token0 == token0.base || cPool.token1 == token0.base) { + foundToken0 = true; + } + if (cPool.token0 == token1.base || cPool.token1 == token1.base) { + foundToken1 = true; + } + } else if (cPool.poolAssets) { + for (let poolAsset_idx = 0; poolAsset_idx < cPool.poolAssets.length; poolAsset_idx++) { + const poolAsset: PoolAsset = cPool.poolAssets[poolAsset_idx]; + if (poolAsset!.token! && poolAsset!.token!.denom) { + if (poolAsset!.token!.denom == token0.base) { + foundToken0 = true; + } + if (poolAsset!.token!.denom == token1.base) { + foundToken1 = true; + } + } + } + } + + if (foundToken0 && foundToken1) { + returnPools.push(new SerializableExtendedPool(cPool)); + if (poolType == 'clmm') { + pricesOut.push( + tickToPrice( + exponentToken0, + exponentToken1, + cPool.currentTick.toString(), + cPool.exponentAtPriceOne.toString(), + ), + ); + } else { + pricesOut.push( + new BigNumber(cPool.poolAssets[0].token.amount) + .dividedBy(new BigNumber(cPool.poolAssets[1].token.amount)) + .toString(), + ); + } + } + }); + + const returnPriceAndPools = { pools: returnPools, prices: pricesOut }; + return returnPriceAndPools; + } catch (error) { + console.debug(error); + } + logger.error('Osmosis: FindPoolsPrices failed, reason unknown.'); + throw new Error('Osmosis: FindPoolsPrices failed, reason unknown.'); + } + + /** + * Returns all pool positions data including number of user's shares, or for single specified poolId + * + * @param req AMM - GetPositionInfoRequestType + * @param return_all? Used internally to resuse function for all positions + */ + async findPoolsPositionsGAMM( + req: AMMGetPositionInfoRequestType, + return_all: boolean = false, + ): Promise { + const address = req.walletAddress; + try { + // only shows GAMM positions by # of poolShares + const balancesContainer = await this._provider.cosmos.bank.v1beta1.allBalances({ + address: address, + pagination: { + key: new Uint8Array(), + offset: BigInt(0), + limit: BigInt(10000), + countTotal: false, + reverse: false, + }, + }); + const balances = balancesContainer.balances; + const lockedCoinsContainer = await this._provider.osmosis.lockup.accountLockedCoins({ + owner: address, + }); + const lockedCoins: Coin[] = lockedCoinsContainer.coins ? lockedCoinsContainer.coins : []; + + // RETURN TYPES: + // concentrated-liquidity/pool || cosmwasmpool/v1beta1/model/pool || gamm/pool-models/balancer/balancerPool || gamm/pool-models/stableswap/stableswap_pool + let poolsContainer; + try { + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } catch (err) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } + const pools: ExtendedPool[] = poolsContainer.pools; + const prices = await this.getCoinGeckPricesStored(); + const fees = await parseFees(pools); + let final_poolAddress; + let filteredPools: ExtendedPool[] = []; + if (req.poolAddress) { + //@ts-expect-error: Osmosis Case 1 + filteredPools = getPoolByAddressAndFilter(this.tokenList, pools, prices, req.poolAddress, false); + if (filteredPools.length > 0) { + final_poolAddress = filteredPools[0].address; + } + } else { + //@ts-expect-error: Osmosis Case 1 + filteredPools = filterPoolsGAMM(this.tokenList, pools, prices, false); // removes stableswap, !token.denom.startsWith('gamm/pool'), has price, has osmosisAsset + } + + const extendedPools = filteredPools.map((pool) => + extendPool(this.assetList, { pool, fees, balances, lockedCoins, prices: prices }), + ); + + // balances contain pool address (as coin denom) and amount (# of shares) + // however it's not returning that for CL pool positions (only GAMM).. so we can't match the pool address to anything here + const returnPools: SerializableExtendedPool[] = []; + extendedPools.forEach(function (cPool) { + if ((cPool.myLiquidity && cPool.myLiquidity != '0') || (cPool.bonded && cPool.bonded != '0')) { + returnPools.push(new SerializableExtendedPool(cPool)); + } else if (final_poolAddress) { + if (cPool.address == final_poolAddress) { + returnPools.push(new SerializableExtendedPool(cPool)); + } + } + }); + + // OSMO AMM pools only return shares, not coins + const final_return: AMMPositionInfo[] = []; + if (return_all) { + returnPools.map((firstPool: SerializableExtendedPool) => { + const poolPrice = new BigNumber(firstPool.poolAssets![0].token.amount) + .dividedBy(new BigNumber(firstPool.poolAssets![1].token.amount)) + .toNumber(); + const returnObj: AMMPositionInfo = { + walletAddress: req.walletAddress, + poolAddress: firstPool.address, + baseTokenAmount: 0, + quoteTokenAmount: 0, + baseTokenAddress: '', + quoteTokenAddress: '', + price: poolPrice, + lpTokenAmount: firstPool.myLiquidityShares, + }; + final_return.push(returnObj); + }); + } else { + if (returnPools.length > 0) { + // we got a poolId but it was for a GAMM pool - so yes poolShares + const firstPool: SerializableExtendedPool = returnPools[0]!; + const poolPrice = new BigNumber(firstPool.poolAssets![0].token.amount) + .dividedBy(new BigNumber(firstPool.poolAssets![1].token.amount)) + .toNumber(); + const returnObj: AMMPositionInfo = { + walletAddress: req.walletAddress, + poolAddress: firstPool.address, + baseTokenAmount: 0, + quoteTokenAmount: 0, + baseTokenAddress: '', + quoteTokenAddress: '', + price: poolPrice, + lpTokenAmount: firstPool.myLiquidityShares, + }; + final_return.push(returnObj); + } + } + + return final_return; + } catch (error) { + console.debug(error + ' ' + error.stack); + } + console.error('Osmosis: FindPoolsPositions failed, reason unknown.'); + } + + /** + * Returns all pool positions data including number of user's shares, or for single specified poolId + * + * @param req CLMM - GetPositionInfoRequestType + * @param return_all? Used internally to resuse function for all positions + */ + async findPoolsPositionsCLMM( + req: CLMMGetPositionInfoRequestType, + return_all: boolean = false, + ): Promise { + const CLMMPositionInfoResponse: CLMMPositionInfo[] = []; + let allCLPositionsBreakDowns = []; + + try { + const typeUrlCLMM = 'osmosis.concentratedliquidity.v1beta1.Pool'; // .token0 .token1 + let poolsContainer; + try { + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } catch (err) { + await new Promise((resolve) => setTimeout(resolve, 1500)); + poolsContainer = await this._provider.osmosis.poolmanager.v1beta1.allPools({}); + } + const pools: ExtendedPool[] = poolsContainer.pools; + const allCLPools = pools.filter(({ $typeUrl }) => $typeUrl?.includes(typeUrlCLMM)); + + if (return_all) { + for (const clPool of allCLPools) { + try { + const allCLPositionsContainer = await this._provider.osmosis.concentratedliquidity.v1beta1.userPositions({ + address: req.walletAddress, + poolId: clPool.id, // UserPositionsRequest requires poolId... so we'll need to query for each CL pool if we do this + }); + if ( + allCLPositionsContainer && + allCLPositionsContainer.positions && + allCLPositionsContainer.positions.length > 0 + ) { + allCLPositionsContainer.positions.map((a) => allCLPositionsBreakDowns.push(a)); //FullPositionBreakdown[] + } + await new Promise((resolve) => setTimeout(resolve, 50)); // RPC will spam ban us if we don't do this. + } catch (error) { + console.debug('Osmosis error from querying all CL pools for active positions. RPC overload?'); // probably spamming RPC too hard at this point + console.debug(error); // probably spamming RPC too hard at this point + } + } + } else { + const clPositionContainer = await this._provider.osmosis.concentratedliquidity.v1beta1.positionById({ + address: req.walletAddress, // requires positionAddress + positionId: req.positionAddress, + }); + if (clPositionContainer && clPositionContainer.position && clPositionContainer.position.position) { + allCLPositionsBreakDowns = [clPositionContainer.position]; + } + } + + for (const clPosition of allCLPositionsBreakDowns) { + const clPoolId = clPosition.position.poolId; + const myPool = pools.find(({ id }) => id == clPoolId); + const lowerPrice = tickToPrice( + this.getExponentByBase(myPool.token0), + this.getExponentByBase(myPool.token1), + clPosition.position.lowerTick.toString(), + myPool.exponentAtPriceOne.toString(), + ); + const upperPrice = tickToPrice( + this.getExponentByBase(myPool.token0), + this.getExponentByBase(myPool.token1), + clPosition.position.upperTick.toString(), + myPool.exponentAtPriceOne.toString(), + ); + const currentPrice = tickToPrice( + this.getExponentByBase(myPool.token0), + this.getExponentByBase(myPool.token1), + myPool.currentTick.toString(), + myPool.exponentAtPriceOne.toString(), + ); + CLMMPositionInfoResponse.push({ + address: clPosition.position.positionId.toString(), // positionId works better as address, positionAddress almost unused + poolAddress: myPool.address, + baseTokenAddress: '', + quoteTokenAddress: '', + baseTokenAmount: Number(clPosition.asset0.amount), + quoteTokenAmount: Number(clPosition.asset1.amount), + baseFeeAmount: 0, + quoteFeeAmount: 0, + lowerBinId: Number(clPosition.position.lowerTick.toString()), + upperBinId: Number(clPosition.position.upperTick.toString()), + lowerPrice: Number(lowerPrice), + upperPrice: Number(upperPrice), + price: Number(currentPrice), + }); + } + return CLMMPositionInfoResponse; + } catch (error) { + console.debug(error); + throw error; + } + } + + async getCurrentBlockNumber(): Promise { + try { + const client = await CosmWasmClient.connect(this.nodeURL); + const getHeight = await client.getHeight(); + return getHeight; + } catch (error) { + console.debug(error); + return 0; // cosmwasm likes to throw 429 on the above call, and we don't actually use this number anywhere on the strat side so this should be ok + } + } + + /** + * Transfer tokens + * + * @param wallet + * @param token + * @param req TransferRequest + */ + async transfer(wallet: CosmosWallet, token: CosmosAsset, req: TransferRequest): Promise { + const keyWallet = await cWalletMaker(wallet.privkey, 'osmo'); + this.signingClient = await this.osmosisGetSigningStargateClient(this.nodeURL, keyWallet.member); + + const tokenInAmount = new BigNumber(req.amount).shiftedBy(token.decimals).toString(); + + const coinIn = { + denom: token.base, + amount: tokenInAmount, + }; + + const coinsList = []; + coinsList.push(coinIn); + const msg = send({ + fromAddress: req.from, + toAddress: req.to, + amount: coinsList, + }); + + const gasAdjustment = this.gasAdjustment; + const feeTier = this.feeTier; + + const enumFee = FEES.osmosis.swapExactAmountIn(feeTier); + let gasToUse = enumFee.gas; + try { + const gasEstimation = await this.signingClient.simulate(req.from, [msg]); + gasToUse = gasEstimation; + } catch (error) { + console.debug(error); + } + + const gasPrice = await this.getLatestBasePrice(); + const calcedFee = calculateFee( + Math.round(Number(gasToUse) * (gasAdjustment || 1.5)), + GasPrice.fromString(gasPrice.toString() + this.manualGasPriceToken), + ); + + if (new BigNumber(calcedFee.gas).gt(new BigNumber(this.gasLimitEstimate))) { + const err = `Osmosis: Transfer failed; Gas limit exceeded ${new BigNumber(calcedFee.gas).toString()} exceeds configured gas limit estimate ${this.gasLimitEstimate}.`; + logger.error(err); + logger.error(err); + throw new Error(err); + } + + const res = await this.signingClient.signAndBroadcast(req.from, [msg], calcedFee); + res.gasPrice = gasPrice; + this.signingClient.disconnect(); + return res; + } + + async getTokens(req?: TokensRequestType): Promise { + const responseTokens: TokenInfo[] = []; + this.tokenList.forEach((element) => { + // FILTER IF req.tokenSymbols != [] + let addToken = true; + if (req && req.tokenSymbols && req.tokenSymbols.length > 0) { + addToken = false; + for (let idx = 0; idx < req.tokenSymbols.length; idx++) { + if (req.tokenSymbols[idx] == element.symbol) { + addToken = true; + break; + } + } + } + if (addToken) { + responseTokens.push({ + chainId: 0, + address: element.address, + name: element.name, + symbol: element.symbol, + decimals: element.decimals, + }); + } + }); + + return { tokens: responseTokens }; + } + + /** + * Gets the allowed slippage percent from the optional parameter or the value + * in the configuration. + * + * @param allowedSlippageStr (Optional) should be of the form '1/10' or '22'. + */ + public getAllowedSlippage(allowedSlippageStr?: string): number { + if (allowedSlippageStr && isFractionString(allowedSlippageStr)) { + const fractionSplit = allowedSlippageStr.split('/'); + return 100 * (Number(fractionSplit[0]) / Number(fractionSplit[1])); + } else if (allowedSlippageStr) { + try { + return Number(allowedSlippageStr); + } catch (err) { + console.debug('Osmosis: Failed to parse allowed slippage input string: ' + allowedSlippageStr); + } + } + + // Use the global allowedSlippage setting + const allowedSlippage = this.allowedSlippage; + const nd = allowedSlippage.match(percentRegexp); + if (nd) return 100 * (Number(nd[1]) / Number(nd[2])); + throw new Error('Encountered a malformed percent string in the config for ALLOWED_SLIPPAGE.'); + } +} diff --git a/src/connectors/osmosis/osmosis.types.ts b/src/connectors/osmosis/osmosis.types.ts new file mode 100755 index 0000000000..dc388d2226 --- /dev/null +++ b/src/connectors/osmosis/osmosis.types.ts @@ -0,0 +1,581 @@ +import { Coin } from '@cosmjs/amino'; +import type { + CoinDenom, + Exponent, + CoinSymbol, + PriceHash, + CoinGeckoToken, + CoinGeckoUSD, + CoinGeckoUSDResponse, + CoinValue, + CoinBalance, + PoolAssetPretty, + PoolTokenImage, + PoolPretty, + CalcPoolAprsParams, + Trade, + PrettyPair, +} from '@osmonauts/math/types'; +import { Type } from '@sinclair/typebox'; +import { Pool as CLPool } from 'osmo-query/osmosis/concentratedliquidity/v1beta1/pool'; +import { CosmWasmPool as CWPool } from 'osmo-query/osmosis/cosmwasmpool/v1beta1/model/pool'; +import { Pool as SSPool } from 'osmo-query/osmosis/gamm/poolmodels/stableswap/v1beta1/stableswap_pool'; +import { Pool as BalancerPool, PoolAsset } from 'osmojs/osmosis/gamm/v1beta1/balancerPool'; + +import { CosmosAsset } from '../../chains/cosmos/cosmos.universaltypes'; +type calcPoolAprs = (...args: any) => any; +export type Pool = BalancerPool & ExtraPoolProperties; +export type AnyPoolType = CLPool | BalancerPool | CWPool | SSPool; +export interface Scenario { + token: CoinBalance; + ratio: string; + symbol: string; + amount: string; + enoughCoinsExist: boolean; + totalDollarValue?: string; +} + +export interface Scenarios { + [key: string]: Scenario[]; +} + +export { + CoinDenom, + Exponent, + CoinSymbol, + PriceHash, + CoinGeckoToken, + CoinGeckoUSD, + CoinGeckoUSDResponse, + CoinValue, + CoinBalance, + PoolAssetPretty, + PoolTokenImage, + PoolPretty, + CalcPoolAprsParams, + Trade, + PrettyPair, +}; + +export type Peroid = '1' | '7' | '14'; + +export type PoolApr = { + [K in Peroid]: ReturnType; // typeof calcPoolAprs +}; + +export type ExtraPoolProperties = { + fees7D: number; + volume24H: number; + volume7d: number; + liquidity: string | number; + myLiquidity?: string | number; + bonded?: string | number; + apr: PoolApr; +}; + +export interface FetchedData { + pools: Pool[]; + prices: PriceHash; + balances: Coin[]; +} + +export interface SwapOptionType { + /** + * Required. Unique identifier for option. + */ + value: string; + /** + * Display symbol name. + */ + symbol: string; + /** + * Icon display for option. + */ + icon?: { + png?: string; + jpeg?: string; + svg?: string; + }; + /** + * Unit of the chain. + */ + denom: string; + amount: string; + displayAmount: string; + dollarValue: string; + chainName: string; +} + +export interface TradeInfo { + baseToken: CosmosAsset; + quoteToken: CosmosAsset; + requestAmount: BigNumber; + expectedTrade: OsmosisExpectedTrade; +} +export interface OsmosisExpectedTrade { + routes: OsmosisExpectedTradeRoute[]; + tokenOutAmount: string; + tokenOutAmountAfterSlippage: string; + executionPrice: BigNumber; + gasLimitEstimate: BigNumber; + tokenInAmount: string; + tokenInAmountAfterSlippage: string; + tokenInDenom: string; + tokenOutDenom: string; + gasUsed: string; + gasWanted: string; + priceImpact: number; +} +export interface OsmosisExpectedTradeRoute { + poolId: string; + swapFee: string; + baseLogo?: { + png?: string; + svg?: string; + jpeg?: string; + }; + baseSymbol: string; + quoteLogo?: { + png?: string; + svg?: string; + jpeg?: string; + }; + quoteSymbol: string; + tokenOutDenom: string; +} + +export interface SwapAmountInRoute { + poolId: bigint; + tokenOutDenom: string; +} + +export interface OsmosisExpectedTradeSwapOut { + routes: { + poolId: string; + swapFee: string; + baseLogo?: { + png?: string; + svg?: string; + jpeg?: string; + }; + baseSymbol: string; + quoteLogo?: { + png?: string; + svg?: string; + jpeg?: string; + }; + quoteSymbol: string; + tokenInDenom: string; + }[]; + expectedAmount: string; + executionPrice: BigNumber; + gasLimitEstimate: BigNumber; + tokenInDenom: string; + tokenOutDenom: string; +} + +export function ToLog_OsmosisExpectedTrade(trade: OsmosisExpectedTrade) { + let output = ''; + trade.routes.forEach((element) => { + output += 'poolId: '; + output += element.poolId; + output += ', '; + output += 'swapFee: '; + output += element.swapFee; + output += ', '; + output += 'baseSymbol: '; + output += element.baseSymbol; + output += ', '; + output += 'quoteSymbol: '; + output += element.quoteSymbol; + output += ', '; + }); + output += 'expectedAmount: '; + output += trade.tokenOutAmount; + output += 'expectedAmountAfterSlippage: '; + output += trade.tokenOutAmountAfterSlippage; + output += ', '; + output += 'executionPrice: '; + output += trade.executionPrice.toString(); + output += ', '; + output += 'gasLimitEstimate: '; + output += trade.gasLimitEstimate.toString(); + output += ', '; + return output; +} + +export type AnyTransactionResponse = + | TransactionResponse + | ReduceLiquidityTransactionResponse + | AddPositionTransactionResponse; + +export interface CoinAndSymbol { + base: string; + amount: string; + symbol: string; +} + +export interface ReduceLiquidityTransactionResponse extends TransactionResponse { + balances: CoinAndSymbol[]; + gasPrice: number; +} + +// returned from transfer() +export interface TransactionResponse { + transactionHash: string; + code: number; + events: TransactionEvent[]; + gasUsed: string; + gasWanted: string; + gasPrice: number; + height: number; + rawLog: string; + feeAmount: string; + // feeAmount: Coin[]; +} + +// poll() +export interface PollTxResponse { + code: number; + codespace: string; + data: string; + events: TransactionEvent[]; + gasUsed: string | bigint; + gasWanted: string | bigint; + height: string | bigint; + info: string; + rawLog: string; + timestamp: string; + txhash: string; +} + +export interface AddLiquidityRequest extends NetworkSelectionRequest { + // now also cosmos add swap position OR cosmos add LP position + address: string; + token0: string; + token1: string; + amount0: string; + amount1: string; + fee?: string; + lowerPrice?: string; // integer as string // COSMOS - using this != undefined then call addpositionLP(), else: addposition() + upperPrice?: string; // integer as string + tokenId?: number; // COSMOS: poolId - will select one for you if not provided + nonce?: number; + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; + allowedSlippage?: string; // COSMOS: used to calc TokenMinAmount + poolId?: string; +} + +export interface AddLiquidityResponse { + network: string; + timestamp: number; + latency: number; + token0: string; + token1: string; + fee: string; + tokenId: number; // COSMOS - this is poolId + gasPrice: number | string; // COSMOS: string + gasPriceToken: string; + gasLimit: number; + gasCost: string; // gasUsed for Cosmos + gasWanted?: string; + nonce: number; + txHash: string | undefined; + poolAddress?: string; // Cosmos only + poolShares?: string; // Cosmos only + token0FinalAmount?: string; // Cosmos only + token1FinalAmount?: string; // Cosmos only +} + +export interface CollectEarnedFeesRequest extends NetworkSelectionRequest { + address: string; + tokenId: number; // COSMOS - this is poolId + nonce?: number; + maxFeePerGas?: string; + maxPriorityFeePerGas?: string; +} + +export interface RemoveLiquidityRequest extends CollectEarnedFeesRequest { + decreasePercent?: number; + allowedSlippage?: string; +} + +export interface RemoveLiquidityResponse { + network: string; + timestamp: number; + latency: number; + tokenId: number; // COSMOS - this is poolId + gasPrice: number | string; // COSMOS: string + gasPriceToken: string; + gasLimit: number | string; + gasCost: string; // gasUsed for Cosmos + nonce?: number; + txHash: string | undefined; + gasWanted?: string; + balances?: CoinAndSymbol[]; + isCollectFees?: boolean; +} + +export interface PositionRequest extends NetworkSelectionRequest { + tokenId?: number; // COSMOS - this is poolId. requried for both + address?: string; // COSMOS only/required (no idea how this works without address for others) +} + +// export interface PollResponse { +// network: string; +// timestamp: number; +// currentBlock: number; +// txHash: string; +// txStatus: number; +// txBlock: number; +// txData: CustomTransactionResponse | null; +// txReceipt: CustomTransactionReceipt | null; +// } + +// export interface CustomTransactionResponse +// extends Omit< +// ethers.providers.TransactionResponse, +// 'gasPrice' | 'gasLimit' | 'value' +// > { +// gasPrice: string | null; +// gasLimit: string; +// value: string; +// } + +// export interface CustomTransactionReceipt +// extends Omit< +// ethers.providers.TransactionReceipt, +// 'gasUsed' | 'cumulativeGasUsed' | 'effectiveGasPrice' +// > { +// gasUsed: string; +// cumulativeGasUsed: string; +// effectiveGasPrice: string | null; +// status: string; +// } + +export interface AddPositionTransactionResponse extends TransactionResponse { + rawLog: string; + poolId: number; // this is GAMM only (sort of, we find it ourselves based on positonId for reducePosition()) + poolAddress: string; + token0_finalamount: string; + token1_finalamount: string; + poolshares: string; + gasPrice: number; + positionId?: number; // this is CL only +} + +export interface TransactionEvent { + attributes: TransactionEventAttribute[]; + type: string; +} +export interface TransactionEventAttribute { + key: string; + value: string; +} + +export interface PriceAndSerializableExtendedPools { + pools: SerializableExtendedPool[]; + prices: string[]; +} + +export class SerializableExtendedPool { + constructor(input: ExtendedPool) { + this.$typeUrl = input.$typeUrl; + this.address = input.address; + this.id = input.id ? input.id.toString() : input.poolId!.toString(); + this.futurePoolGovernor = input.futurePoolGovernor; + this.totalShares = input.totalShares; + this.token0 = input.token0; + this.token1 = input.token1; + this.poolAssets = input.poolAssets; + this.totalWeight = input.totalWeight; + this.fees_volume24H = input.volume24H; + this.fees_spent_7d = input.fees7D; + this.fees_volume7d = input.volume7d; + this.myLiquidityShares = input.myLiquidity ? Number(input.myLiquidity) : 0; + this.myLiquidityDollarValue = input.myLiquidityDollarValue; + this.my_bonded_shares = input.bonded; + this.denom = input.denom; + this.currentTick = input.currentTick ? input.currentTick.toString() : '0'; + this.exponentAtPriceOne = input.exponentAtPriceOne ? input.exponentAtPriceOne.toString() : '0'; + this.swapFee = input.poolParams ? input.poolParams.swapFee : '0'; + this.exitFee = input.poolParams ? input.poolParams.exitFee : '0'; + this.tickSpacing = input.tickSpacing ? input.tickSpacing.toString() : '0'; + this.incentivesAddress = input.incentivesAddress; + this.spreadRewardsAddress = input.spreadRewardsAddress; + this.currentTickLiquidity = input.currentTickLiquidity; + this.poolLiquidity = input.poolLiquidity; + } + $typeUrl?: string; + address: string; + incentivesAddress?: string = ''; // CL + spreadRewardsAddress?: string = ''; // CL + id: string; + // poolParams: PoolParams; + futurePoolGovernor?: string; + totalShares?: Coin; + poolAssets?: PoolAsset[]; + totalWeight?: string; + token0?: string; + token1?: string; + currentTick?: string; + tickSpacing?: string; + exponentAtPriceOne?: string; + fees_volume24H: number; + fees_spent_7d: number; + fees_volume7d: number; + currentTickLiquidity: string; + myLiquidityShares: number; + myLiquidityDollarValue: string; + my_bonded_shares: string; + denom: string; + swapFee: string = ''; + exitFee: string = ''; + poolLiquidity: Coin[] = [{ amount: '', denom: '' }]; // stableswap +} + +export class ExtendedPool { + liquidity: number = 0; + volume24H: number = 0; + fees7D: number = 0; + volume7d: number = 0; + myLiquidity: string = ''; + myLiquidityDollarValue: string = ''; + bonded: string = ''; + denom: string = ''; + $typeUrl?: string | undefined; + address: string = ''; + incentivesAddress: string = ''; // CL + spreadRewardsAddress: string = ''; // CL + totalWeight: string = ''; + token0: string = ''; // base token + token1: string = ''; // quote token + poolAssets: PoolAsset[] = []; + id: bigint = BigInt(0); + poolId: bigint = BigInt(0); + currentTick: bigint = BigInt(0); + exponentAtPriceOne: bigint = BigInt(0); + poolParams: PoolParams = new PoolParams(); + futurePoolGovernor: string = ''; + totalShares: Coin = { amount: '', denom: '' }; + poolLiquidity: Coin[] = [{ amount: '', denom: '' }]; // stableswap + swapFee: string = ''; + exitFee: string = ''; + tickSpacing: bigint = BigInt(0); + currentTickLiquidity: string = ''; +} + +class PoolParams { + swapFee: string = ''; + exitFee: string = ''; + smoothWeightChangeParams: undefined; +} + +export const FetchPoolsRequest = Type.Object({ + network: Type.Optional( + Type.String({ + description: 'Osmosis network to use', + default: 'mainnet', + enum: ['mainnet', 'testnet'], + }), + ), + limit: Type.Optional( + Type.Number({ + minimum: 1, + default: 10, + description: 'Maximum number of pools to return', + examples: [10], + }), + ), + tokenA: Type.Optional( + Type.String({ + description: 'First token symbol or address', + examples: ['ION'], + }), + ), + tokenB: Type.Optional( + Type.String({ + description: 'Second token symbol or address', + examples: ['OSMO'], + }), + ), +}); + +// TYPES BORROWED FROM PREVIOUS HBOT VERSION since I had to recitfy all my code to work with them anyway... +// TYPES BORROWED FROM PREVIOUS HBOT VERSION since I had to recitfy all my code to work with them anyway... + +export interface PositionInfo { + token0?: string | undefined; + token1?: string | undefined; + poolShares?: string; // COSMOS - GAMM pools only issue poolShares (no amount/unclaimedToken) + fee?: string | undefined; + lowerPrice?: string; + upperPrice?: string; + amount0?: string; // COSMOS - CL pools only + amount1?: string; // COSMOS - CL pools only + unclaimedToken0?: string; // COSMOS - CL pools only + unclaimedToken1?: string; // COSMOS - CL pools only + pools?: SerializableExtendedPool[]; +} + +export interface PositionResponse extends PositionInfo { + network: string; + timestamp: number; + latency: number; +} + +export interface NetworkSelectionRequest { + chain: string; //the target chain (e.g. ethereum, avalanche, or harmony) + network: string; // the target network of the chain (e.g. mainnet) + connector?: string; //the target connector (e.g. uniswap or pangolin) +} +export interface TransferRequest extends NetworkSelectionRequest { + to: string; + from: string; + amount: string; + token: string; +} + +export type TransferResponse = string | FullTransferResponse; + +export interface FullTransferResponse { + network: string; + timestamp: number; + latency: number; + amount: string; + gasPrice: string; + gasLimit: string; + gasUsed: string; + gasWanted: string; + txHash: string; +} +export type OrderType = 'LIMIT' | 'LIMIT_MAKER'; +export type Side = 'BUY' | 'SELL'; +export type PerpSide = 'LONG' | 'SHORT'; + +export interface PoolPriceRequest extends NetworkSelectionRequest { + token0: string; + token1: string; + address?: string; + fee?: string; + period?: number; + interval?: number; + poolId?: string; +} + +export interface PoolPriceResponse { + token0: string; + token1: string; + fee?: string; + period?: number; + interval?: number; + prices?: string[]; + pools?: SerializableExtendedPool[]; + network: string; + timestamp: number; + latency: number; +} + +// TYPES BORROWED FROM PREVIOUS HBOT VERSION since I had to recitfy all my code to work with them anyway... +// TYPES BORROWED FROM PREVIOUS HBOT VERSION since I had to recitfy all my code to work with them anyway... diff --git a/src/connectors/osmosis/osmosis.utils.ts b/src/connectors/osmosis/osmosis.utils.ts new file mode 100755 index 0000000000..d8b496d136 --- /dev/null +++ b/src/connectors/osmosis/osmosis.utils.ts @@ -0,0 +1,173 @@ +import { BigNumber } from 'bignumber.js'; + +type PoolReward = { + day_usd: number; + month_usd: number; + year_usd: number; +}; + +type Rewards = { + pools: { + [key: number]: PoolReward; + }; + total_day_usd: number; + total_month_usd: number; + total_year_usd: number; +}; + +export const fetchRewards = async (address: string): Promise => { + const url = `https://api-osmosis-chain.imperator.co/lp/v1/rewards/estimation/${address}`; + return ( + fetch(url) + // .then(handleError) + .then((res) => res.json()) + ); +}; + +export function calculatePriceToTick( + desiredPriceString: string, + exponentAtPriceOne: number, + tickSpacing: number, + is_lowerBound: boolean, +): string { + console.log( + `Inputs: desiredPriceString=${desiredPriceString}, exponentAtPriceOne=${exponentAtPriceOne}, tickSpacing=${tickSpacing}, is_lowerBound=${is_lowerBound}`, + ); + + const desiredPrice = new BigNumber(desiredPriceString); + const exponent = new BigNumber(exponentAtPriceOne); + const geometricExponentIncrementDistanceInTicks = new BigNumber(9).multipliedBy( + new BigNumber(10).exponentiatedBy(exponent.multipliedBy(-1)), + ); + + console.log( + `Initial calculations: desiredPrice=${desiredPrice}, exponent=${exponent}, geometricExponentIncrementDistanceInTicks=${geometricExponentIncrementDistanceInTicks}`, + ); + + let currentPrice = new BigNumber(1); + let ticksPassed = new BigNumber(0); + let exponentAtCurrentTick = exponent; + let currentAdditiveIncrementInTicks = new BigNumber(10).exponentiatedBy(exponent); + + if (desiredPrice.gt(new BigNumber(1))) { + while (currentPrice.lt(desiredPrice)) { + currentAdditiveIncrementInTicks = new BigNumber(10).exponentiatedBy(exponentAtCurrentTick); + const maxPriceForCurrentAdditiveIncrementInTicks = geometricExponentIncrementDistanceInTicks.multipliedBy( + currentAdditiveIncrementInTicks, + ); + currentPrice = currentPrice.plus(maxPriceForCurrentAdditiveIncrementInTicks); + exponentAtCurrentTick = exponentAtCurrentTick.plus(1); + ticksPassed = ticksPassed.plus(geometricExponentIncrementDistanceInTicks); + + console.log( + `Loop (desiredPrice > 1): currentPrice=${currentPrice}, exponentAtCurrentTick=${exponentAtCurrentTick}, ticksPassed=${ticksPassed}`, + ); + } + } else { + exponentAtCurrentTick = exponent.minus(1); + while (currentPrice.gt(desiredPrice)) { + currentAdditiveIncrementInTicks = new BigNumber(10).exponentiatedBy(exponentAtCurrentTick); + const maxPriceForCurrentAdditiveIncrementInTicks = geometricExponentIncrementDistanceInTicks.multipliedBy( + currentAdditiveIncrementInTicks, + ); + currentPrice = currentPrice.minus(maxPriceForCurrentAdditiveIncrementInTicks); + exponentAtCurrentTick = exponentAtCurrentTick.minus(1); + ticksPassed = ticksPassed.minus(geometricExponentIncrementDistanceInTicks); + + console.log( + `Loop (desiredPrice <= 1): currentPrice=${currentPrice}, exponentAtCurrentTick=${exponentAtCurrentTick}, ticksPassed=${ticksPassed}`, + ); + } + } + + const ticksToBeFulfilledByExponentAtCurrentTick = desiredPrice + .minus(currentPrice) + .dividedBy(currentAdditiveIncrementInTicks); + console.log(`Ticks to be fulfilled by current exponent: ${ticksToBeFulfilledByExponentAtCurrentTick}`); + + const tickIndex = ticksPassed.plus(ticksToBeFulfilledByExponentAtCurrentTick); + console.log(`Tick index: ${tickIndex}`); + + let returnTick = new BigNumber(0); + if (is_lowerBound) { + returnTick = tickIndex.dividedBy(tickSpacing).integerValue(BigNumber.ROUND_DOWN).multipliedBy(tickSpacing); + } else { + returnTick = tickIndex.dividedBy(tickSpacing).integerValue(BigNumber.ROUND_CEIL).multipliedBy(tickSpacing); + } + + console.log(`Final calculations: tickIndex=${tickIndex}, returnTick=${BigInt(returnTick.toNumber()).toString()}`); + if (returnTick.isEqualTo(0)) { + throw new Error('Osmosis: Failed to find tick within price bounds for CL Position Open.'); + } + return BigInt(returnTick.toNumber()).toString(); +} + +export function tickToPrice( + exponentToken0: number, + exponentToken1: number, + currentTickIn: string, + exponentAtPriceOne: string, +): string { + const currentTick = new BigNumber(currentTickIn); + const exponent = new BigNumber(exponentAtPriceOne); // -6 + + const geoExponentIncrementTicks = new BigNumber(9).multipliedBy( + new BigNumber(10).exponentiatedBy(exponent.multipliedBy(-1)), + ); // 9e6 + const geoExponentDelta = currentTick.dividedBy(geoExponentIncrementTicks).integerValue(BigNumber.ROUND_FLOOR); + + const exponentAtCurrentTick = new BigNumber(exponentAtPriceOne).plus(geoExponentDelta); + const currentAddIncrementTicks = new BigNumber(10).exponentiatedBy(exponentAtCurrentTick); // 10e-6 + + const numAdditiveTicks = currentTick.minus(geoExponentDelta.multipliedBy(geoExponentIncrementTicks)); + + let price = new BigNumber(10) + .exponentiatedBy(geoExponentDelta) + .plus(numAdditiveTicks.multipliedBy(currentAddIncrementTicks)); + + price = price.dividedBy( + new BigNumber(10).exponentiatedBy(exponentToken1).dividedBy(new BigNumber(10).exponentiatedBy(exponentToken0)), + ); + + return price.toString(); +} + +export function findTickForPrice( + desiredPriceString: string, + exponentAtPriceOne: number, + tickSpacing: number, + is_lowerBound: boolean, +): string { + const desiredPrice = new BigNumber(desiredPriceString); + let exponent = new BigNumber(exponentAtPriceOne); // -6 + const geoExponentIncrementTicks = new BigNumber(9).multipliedBy( + new BigNumber(10).exponentiatedBy(exponent.multipliedBy(-1)), + ); // 9e6 + + const currentPrice = new BigNumber(1); + let ticksPassed = new BigNumber(0); + let currentAddIncrementTicks = new BigNumber(10).exponentiatedBy(exponent); // 10e-6 + let currentAddIncrementTicksPrice = currentAddIncrementTicks.multipliedBy(geoExponentIncrementTicks); // 9 + + ticksPassed = ticksPassed.plus(geoExponentIncrementTicks); // 9e6 + let totalPrice = currentPrice.plus(currentAddIncrementTicksPrice); // 10 + + while (totalPrice.isLessThan(desiredPrice)) { + exponent = exponent.plus(1); + currentAddIncrementTicks = new BigNumber(10).exponentiatedBy(exponent); + currentAddIncrementTicksPrice = currentAddIncrementTicks.multipliedBy(geoExponentIncrementTicks); + ticksPassed = ticksPassed.plus(geoExponentIncrementTicks); + totalPrice = totalPrice.plus(currentAddIncrementTicksPrice); + } + + const ticksToBeFulfilledByExponentAtCurrentTick = desiredPrice.minus(totalPrice).dividedBy(currentAddIncrementTicks); + const tickIndex = ticksPassed.plus(ticksToBeFulfilledByExponentAtCurrentTick); + + let returnTick; + if (is_lowerBound) { + returnTick = tickIndex.dividedBy(tickSpacing).integerValue(BigNumber.ROUND_DOWN).multipliedBy(tickSpacing); + } else { + returnTick = tickIndex.dividedBy(tickSpacing).integerValue(BigNumber.ROUND_CEIL).multipliedBy(tickSpacing); + } + return returnTick.toString(); +} diff --git a/src/connectors/osmosis/router-routes/executeSwap.ts b/src/connectors/osmosis/router-routes/executeSwap.ts new file mode 100755 index 0000000000..2c3b9e66b4 --- /dev/null +++ b/src/connectors/osmosis/router-routes/executeSwap.ts @@ -0,0 +1,86 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { ExecuteSwapRequestType, ExecuteSwapResponseType, ExecuteSwapResponse } from '../../../schemas/clmm-schema'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; +import { executeSwap } from '../osmosis.swap'; + +export const executeSwapRoute: FastifyPluginAsync = async (fastify, _options) => { + // Import the httpErrors plugin to ensure it's available + await fastify.register(require('@fastify/sensible')); + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + // Get available networks from Osmosis configuration (same method as chain.routes.ts) + const { ConfigManagerV2 } = require('../../../services/config-manager-v2'); + const osmosisNetworks = ['testnet', 'mainnet']; + + fastify.post<{ + Body: ExecuteSwapRequestType; + Reply: ExecuteSwapResponseType; + }>( + '/execute-swap', + { + schema: { + description: 'Execute a swap using Osmosis Order Router', + tags: ['osmosis'], + body: { + type: 'object', + properties: { + network: { + type: 'string', + default: 'mainnet', + enum: osmosisNetworks, + }, + walletAddress: { type: 'string', examples: [walletAddressExample] }, + baseToken: { type: 'string', examples: ['ION'] }, + quoteToken: { type: 'string', examples: ['OSMO'] }, + amount: { type: 'number', examples: [0.001] }, + side: { type: 'string', enum: ['BUY', 'SELL'], examples: ['SELL'] }, + slippagePct: { type: 'number', examples: [0.5] }, + }, + required: ['walletAddress', 'baseToken', 'quoteToken', 'amount', 'side'], + }, + response: { + 200: ExecuteSwapResponse, + }, + }, + }, + async (request, reply) => { + try { + // Log the request parameters for debugging + logger.info(`Received execute-swap request: ${JSON.stringify(request.body)}`); + const { + network, + walletAddress, + baseToken: baseTokenSymbol, + quoteToken: quoteTokenSymbol, + amount, + side, + slippagePct, + } = request.body; + + // Validate essential parameters + if (!baseTokenSymbol || !quoteTokenSymbol || !amount || !side || !network || !slippagePct || !walletAddress) { + logger.error('Missing required parameters in request'); + return reply.badRequest('Missing required parameters'); + } + + const executeSwapResponse = await executeSwap(fastify, request.body, 'router'); + return executeSwapResponse; + } catch (e) { + logger.error(`Execute swap error: ${e.message}`); + if (e.stack) { + logger.debug(`Error stack: ${e.stack}`); + } + + if (e.code === 'UNPREDICTABLE_GAS_LIMIT') { + return reply.badRequest('Transaction failed: Insufficient funds or gas estimation error'); + } + + return reply.internalServerError(`Failed to execute swap: ${e.message}`); + } + }, + ); +}; + +export default executeSwapRoute; diff --git a/src/connectors/osmosis/router-routes/index.ts b/src/connectors/osmosis/router-routes/index.ts new file mode 100644 index 0000000000..e47988ab11 --- /dev/null +++ b/src/connectors/osmosis/router-routes/index.ts @@ -0,0 +1,13 @@ +import { FastifyPluginAsync } from 'fastify'; + +// import executeQuoteRoute from './executeQuote'; +import { executeSwapRoute } from './executeSwap'; +import { quoteSwapRoute } from './quoteSwap'; + +export const osmosisRouterRoutes: FastifyPluginAsync = async (fastify) => { + await fastify.register(quoteSwapRoute); + // await fastify.register(executeQuoteRoute); + await fastify.register(executeSwapRoute); +}; + +export default osmosisRouterRoutes; diff --git a/src/connectors/osmosis/router-routes/quoteSwap.ts b/src/connectors/osmosis/router-routes/quoteSwap.ts new file mode 100755 index 0000000000..d042663ffa --- /dev/null +++ b/src/connectors/osmosis/router-routes/quoteSwap.ts @@ -0,0 +1,122 @@ +import { FastifyPluginAsync } from 'fastify'; + +import { QuoteSwapResponseType, QuoteSwapResponse, QuoteSwapRequestType } from '../../../schemas/clmm-schema'; +import { ConfigManagerV2 } from '../../../services/config-manager-v2'; +import { logger } from '../../../services/logger'; +import { Osmosis } from '../osmosis'; +import { quoteSwap } from '../osmosis.swap'; + +export const quoteSwapRoute: FastifyPluginAsync = async (fastify, _options) => { + // Import the httpErrors plugin to ensure it's available + await fastify.register(require('@fastify/sensible')); + + // Get first wallet address for example + const walletAddressExample = await Osmosis.getWalletAddressExample(); + + // Get available networks from osmosis configuration (same method as chain.routes.ts) + const osmosisNetworks = ['testnet', 'mainnet']; + + fastify.get<{ + Querystring: QuoteSwapRequestType; + Reply: QuoteSwapResponseType; + }>( + '/quote-swap', + { + schema: { + description: 'Get a swap quote using Osmosis router', + tags: ['/connectors/osmosis/swap'], + querystring: { + type: 'object', + properties: { + network: { + type: 'string', + default: 'mainnet', + enum: osmosisNetworks, + }, + baseToken: { type: 'string', examples: ['ION'] }, + quoteToken: { type: 'string', examples: ['OSMO'] }, + 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'], + }, + response: { + 200: QuoteSwapResponse, + }, + }, + }, + async (request, reply) => { + try { + // Log the request parameters for debugging + logger.info(`Received quote-swap request: ${JSON.stringify(request.query)}`); + + const { + network, + baseToken: baseTokenSymbol, + quoteToken: quoteTokenSymbol, + amount, + side, + slippagePct, + } = request.query; + + // Validate essential parameters + if (!baseTokenSymbol || !quoteTokenSymbol || !amount || !side || !network || !slippagePct) { + logger.error('Missing required parameters in request'); + return reply.badRequest('Missing required parameters'); + } + + try { + // Use our shared quote function + const quoteResult = await quoteSwap(fastify, request.query, 'router'); + + // Return only the data needed for the API response + return { + slippagePct: quoteResult.slippagePct, + poolAddress: quoteResult.poolAddress, + tokenIn: request.query.baseToken, + tokenOut: request.query.quoteToken, + amountIn: quoteResult.amountIn, + amountOut: quoteResult.amountOut, + price: quoteResult.price, + minAmountOut: quoteResult.minAmountOut, + maxAmountIn: quoteResult.maxAmountIn, + priceImpactPct: quoteResult.priceImpactPct, + }; + } catch (error) { + // If the error already has a status code, it's a Fastify HTTP error + if (error.statusCode) { + throw error; + } + + // Log more detailed information about the error + logger.error(`Router error: ${error.message}`); + if (error.stack) { + logger.debug(`Error stack: ${error.stack}`); + } + + // Check if there's any additional error details + if (error.innerError) { + logger.error(`Inner error: ${JSON.stringify(error.innerError)}`); + } + + // Check if it's a specific error type from the Alpha Router + if (error.name === 'SwapRouterError') { + logger.error(`SwapRouterError details: ${JSON.stringify(error)}`); + } + + return reply.badRequest(`Failed to get quote with router: ${error.message}`); + } + } catch (e) { + logger.error(`Quote swap error: ${e.message}`); + if (e.stack) { + logger.debug(`Error stack: ${e.stack}`); + } + return reply.internalServerError(`Failed to get quote: ${e.message}`); + } + }, + ); +}; + +export default quoteSwapRoute; diff --git a/src/connectors/pancakeswap-sol/clmm-routes/executeSwap.ts b/src/connectors/pancakeswap-sol/clmm-routes/executeSwap.ts index e2af6d6ba3..9fe105776f 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/executeSwap.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/executeSwap.ts @@ -28,7 +28,7 @@ export async function executeSwap( slippagePct?: number, ): Promise { // Get quote first - this contains all the slippage calculations and pool lookup - const { quoteSwap } = await import('./quoteSwap'); + const { quoteSwap } = await import('./quoteSwap.js'); const quote = await quoteSwap( fastify, network, diff --git a/src/connectors/pancakeswap-sol/clmm-routes/quoteSwap.ts b/src/connectors/pancakeswap-sol/clmm-routes/quoteSwap.ts index 1f3c44472c..18f652a5ab 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/quoteSwap.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/quoteSwap.ts @@ -46,7 +46,7 @@ export async function quoteSwap( // If no pool address provided, try to find it from pool service let poolAddressToUse = poolAddress; if (!poolAddressToUse) { - const { PoolService } = await import('../../../services/pool-service'); + const { PoolService } = await import('../../../services/pool-service.js'); const poolService = PoolService.getInstance(); const pool = await poolService.getPool('pancakeswap-sol', network, 'clmm', baseToken.symbol, quoteToken.symbol); diff --git a/src/connectors/pancakeswap-sol/pancakeswap-sol.instructions.ts b/src/connectors/pancakeswap-sol/pancakeswap-sol.instructions.ts index b567a6a7de..4d2106f6c8 100644 --- a/src/connectors/pancakeswap-sol/pancakeswap-sol.instructions.ts +++ b/src/connectors/pancakeswap-sol/pancakeswap-sol.instructions.ts @@ -106,7 +106,7 @@ export async function buildSwapV2Instruction( const outputVault = outputMint.equals(tokenMint0) ? tokenVault0 : tokenVault1; // Debug logging - const { logger } = await import('../../services/logger'); + const { logger } = await import('../../services/logger.js'); logger.info(`Pool tokens: mint0=${tokenMint0.toString()}, mint1=${tokenMint1.toString()}`); logger.info(`Swap tokens: input=${inputMint.toString()}, output=${outputMint.toString()}`); logger.info(`Vaults: input=${inputVault.toString()}, output=${outputVault.toString()}`); @@ -586,7 +586,7 @@ export async function buildOpenPositionWithToken22NftInstruction( const tokenAccount1 = getAssociatedTokenAddressSync(tokenMint1, walletPubkey, false, tokenProgram1); // Log all instruction parameters - const { logger } = await import('../../services/logger'); + const { logger } = await import('../../services/logger.js'); logger.info(`=== OpenPosition Instruction Parameters ===`); logger.info(`Pool: ${poolAddress.toString()}`); logger.info(`Pool tokens: mint0=${tokenMint0.toString()}, mint1=${tokenMint1.toString()}`); diff --git a/src/connectors/pancakeswap/pancakeswap.ts b/src/connectors/pancakeswap/pancakeswap.ts index 0bb7bf8063..9cb58548c5 100644 --- a/src/connectors/pancakeswap/pancakeswap.ts +++ b/src/connectors/pancakeswap/pancakeswap.ts @@ -404,7 +404,7 @@ export class Pancakeswap { ); // Use PoolService to find pool by token pair - const { PoolService } = await import('../../services/pool-service'); + const { PoolService } = await import('../../services/pool-service.js'); const poolService = PoolService.getInstance(); const pool = await poolService.getPool( diff --git a/src/connectors/raydium/amm-routes/executeSwap.ts b/src/connectors/raydium/amm-routes/executeSwap.ts index 1b7eeb0de0..b307468491 100644 --- a/src/connectors/raydium/amm-routes/executeSwap.ts +++ b/src/connectors/raydium/amm-routes/executeSwap.ts @@ -224,7 +224,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { } // Use PoolService to find pool by token pair - const { PoolService } = await import('../../../services/pool-service'); + const { PoolService } = await import('../../../services/pool-service.js'); const poolService = PoolService.getInstance(); const pool = await poolService.getPool( diff --git a/src/connectors/raydium/amm-routes/quoteSwap.ts b/src/connectors/raydium/amm-routes/quoteSwap.ts index 26857bc183..86f0d768e8 100644 --- a/src/connectors/raydium/amm-routes/quoteSwap.ts +++ b/src/connectors/raydium/amm-routes/quoteSwap.ts @@ -524,7 +524,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { } // Use PoolService to find pool by token pair - const { PoolService } = await import('../../../services/pool-service'); + const { PoolService } = await import('../../../services/pool-service.js'); const poolService = PoolService.getInstance(); const pool = await poolService.getPool( diff --git a/src/connectors/raydium/clmm-routes/executeSwap.ts b/src/connectors/raydium/clmm-routes/executeSwap.ts index bfc3c46c76..991c4e43b1 100644 --- a/src/connectors/raydium/clmm-routes/executeSwap.ts +++ b/src/connectors/raydium/clmm-routes/executeSwap.ts @@ -224,7 +224,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { } // Use PoolService to find pool by token pair - const { PoolService } = await import('../../../services/pool-service'); + const { PoolService } = await import('../../../services/pool-service.js'); const poolService = PoolService.getInstance(); const pool = await poolService.getPool( diff --git a/src/connectors/raydium/clmm-routes/quoteSwap.ts b/src/connectors/raydium/clmm-routes/quoteSwap.ts index 4df87bdca7..aae19178ad 100644 --- a/src/connectors/raydium/clmm-routes/quoteSwap.ts +++ b/src/connectors/raydium/clmm-routes/quoteSwap.ts @@ -320,7 +320,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { } // Use PoolService to find pool by token pair - const { PoolService } = await import('../../../services/pool-service'); + const { PoolService } = await import('../../../services/pool-service.js'); const poolService = PoolService.getInstance(); const pool = await poolService.getPool( diff --git a/src/connectors/uniswap/uniswap.ts b/src/connectors/uniswap/uniswap.ts index b7b0eda973..495bf9817b 100644 --- a/src/connectors/uniswap/uniswap.ts +++ b/src/connectors/uniswap/uniswap.ts @@ -193,7 +193,7 @@ export class Uniswap { const tokenForAmount = exactIn ? inputToken : outputToken; // Convert amount to token units using ethers parseUnits for proper decimal handling - const { parseUnits } = await import('ethers/lib/utils'); + const { parseUnits } = await import('ethers/lib/utils.js'); const rawAmount = parseUnits(amount.toString(), tokenForAmount.decimals); const tradeAmount = CurrencyAmount.fromRawAmount(tokenForAmount, rawAmount.toString()); @@ -398,7 +398,7 @@ export class Uniswap { ); // Use PoolService to find pool by token pair - const { PoolService } = await import('../../services/pool-service'); + const { PoolService } = await import('../../services/pool-service.js'); const poolService = PoolService.getInstance(); const pool = await poolService.getPool( diff --git a/src/pools/pool-info-helpers.ts b/src/pools/pool-info-helpers.ts index 89a3d6088a..18207a0c80 100644 --- a/src/pools/pool-info-helpers.ts +++ b/src/pools/pool-info-helpers.ts @@ -2,7 +2,7 @@ * Helper functions for fetching pool info from connectors */ -import { FastifyInstance } from 'fastify'; +import { Osmosis } from '#src/connectors/osmosis/osmosis.js'; import { Ethereum } from '../chains/ethereum/ethereum'; import { Solana } from '../chains/solana/solana'; @@ -20,12 +20,12 @@ interface PoolInfoResult { /** * Get chain type for a connector from config */ -function getConnectorChain(connector: string): 'solana' | 'ethereum' | null { +function getConnectorChain(connector: string): 'solana' | 'ethereum' | 'cosmos' | null { const config = connectorsConfig.find((c) => c.name === connector); if (!config) { return null; } - return config.chain as 'solana' | 'ethereum'; + return config.chain as 'solana' | 'ethereum' | 'cosmos'; } /** @@ -146,6 +146,14 @@ export async function fetchPoolInfo( feePct: feePct, }; } + } else if (connector === 'osmosis') { + const osmosis = await Osmosis.getInstance(network); + const poolInfo = await osmosis.controller.poolInfoRequest(osmosis, undefined, { network, poolAddress }, type); + return { + baseTokenAddress: poolInfo.baseTokenAddress, + quoteTokenAddress: poolInfo.quoteTokenAddress, + feePct: poolInfo.feePct, + }; } else { logger.error(`Unsupported chain: ${chain} for connector: ${connector}`); return null; diff --git a/src/schemas/clmm-schema.ts b/src/schemas/clmm-schema.ts index b5b2cb7bcd..4f6163dbc6 100644 --- a/src/schemas/clmm-schema.ts +++ b/src/schemas/clmm-schema.ts @@ -1,7 +1,5 @@ import { Type, Static } from '@sinclair/typebox'; -import { TransactionStatus } from './chain-schema'; - export const FetchPoolsRequest = Type.Object( { network: Type.Optional(Type.String()), // Network @@ -167,6 +165,7 @@ export const AddLiquidityResponse = Type.Object( fee: Type.Number(), baseTokenAmountAdded: Type.Number(), quoteTokenAmountAdded: Type.Number(), + newPositionAddress: Type.Optional(Type.String()), // Osmosis - returns new position address on AddLiqudity (and exclusively on AddLiquidity) }), ), }, diff --git a/src/services/config-manager-v2.ts b/src/services/config-manager-v2.ts index 832050da1e..7a1a750ee1 100644 --- a/src/services/config-manager-v2.ts +++ b/src/services/config-manager-v2.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; -import Ajv, { ValidateFunction, DefinedError } from 'ajv'; +import { Ajv, ValidateFunction, DefinedError } from 'ajv'; import fse from 'fs-extra'; import yaml from 'js-yaml'; @@ -405,7 +405,8 @@ export class ConfigManagerV2 { unpackFullConfigPath(fullConfigPath: string): UnpackedConfigNamespace { const pathComponents: Array = fullConfigPath.split('.'); if (pathComponents.length < 2) { - throw new Error('Configuration paths must have at least two components.'); + console.log(fullConfigPath); + throw new Error(`Configuration paths must have at least two components (received ${fullConfigPath}).`); } const namespaceComponent: string = pathComponents[0]; diff --git a/src/services/connection-manager.ts b/src/services/connection-manager.ts index 400fe795c7..754894d4e7 100644 --- a/src/services/connection-manager.ts +++ b/src/services/connection-manager.ts @@ -1,11 +1,12 @@ import { Ethereum } from '../chains/ethereum/ethereum'; import { Solana } from '../chains/solana/solana'; +import { Osmosis } from '../connectors/osmosis/osmosis'; export interface Chain { // TODO: Add shared chain properties (e.g., network, chainId, etc.) } -export type ChainInstance = Ethereum | Solana; +export type ChainInstance = Ethereum | Solana | Osmosis; export class UnsupportedChainException extends Error { constructor(message?: string) { @@ -32,7 +33,7 @@ export async function getInitializedChain<_T>(chain: string, network: string): P */ export function getSupportedChains(): string[] { // These should match the chains in getChainInstance - return ['ethereum', 'solana']; + return ['ethereum', 'solana', 'cosmos']; } export async function getChainInstance(chain: string, network: string): Promise { @@ -43,6 +44,8 @@ export async function getChainInstance(chain: string, network: string): Promise< connection = await Ethereum.getInstance(network); } else if (chainLower === 'solana') { connection = await Solana.getInstance(network); + } else if (chainLower === 'cosmos') { + connection = await Osmosis.getInstance(network); } else { connection = undefined; } @@ -61,13 +64,13 @@ export async function getConnector( ): Promise { // Dynamically import connector classes only when needed if (connector === 'uniswap') { - const { Uniswap } = await import('../connectors/uniswap/uniswap'); + const { Uniswap } = await import('../connectors/uniswap/uniswap.js'); return await Uniswap.getInstance(network); } else if (connector === 'jupiter') { - const { Jupiter } = await import('../connectors/jupiter/jupiter'); + const { Jupiter } = await import('../connectors/jupiter/jupiter.js'); return await Jupiter.getInstance(network); } else if (connector === 'meteora') { - const { Meteora } = await import('../connectors/meteora/meteora'); + const { Meteora } = await import('../connectors/meteora/meteora.js'); return await Meteora.getInstance(network); } else { throw new Error('unsupported chain or connector'); diff --git a/src/services/pool-service.ts b/src/services/pool-service.ts index 610cd5be28..0dcf96610f 100644 --- a/src/services/pool-service.ts +++ b/src/services/pool-service.ts @@ -98,6 +98,8 @@ export class PoolService { return SupportedChain.ETHEREUM; case 'solana': return SupportedChain.SOLANA; + case 'cosmos': + return SupportedChain.COSMOS; default: throw new Error(`Unsupported chain '${connectorInfo.chain}' for connector: ${connector}`); } diff --git a/src/services/startup-banner.ts b/src/services/startup-banner.ts index c3cb0a669a..3822124e51 100644 --- a/src/services/startup-banner.ts +++ b/src/services/startup-banner.ts @@ -7,6 +7,7 @@ import { InfuraService } from '../chains/ethereum/infura-service'; import { HeliusService } from '../chains/solana/helius-service'; import { Solana } from '../chains/solana/solana'; import { getSolanaNetworkConfig } from '../chains/solana/solana.config'; +import { Osmosis } from '../connectors/osmosis/osmosis'; import { ConfigManagerV2 } from './config-manager-v2'; import { logger, redactUrl } from './logger'; @@ -23,6 +24,9 @@ export async function displayChainConfigurations(): Promise { // Display Ethereum configuration await displayEthereumConfig(); + + // Display Cosmos configuration + await displayCosmosConfig(); } catch (error: any) { logger.warn(`Failed to display chain configurations: ${error.message}`); } @@ -139,3 +143,38 @@ async function displayEthereumConfig(): Promise { logger.debug(`Ethereum configuration not available: ${error.message}`); } } + +/** + * Display Cosmos chain configuration, using only Osmosis + * We can do this via Cosmos.getInstance(defaultNetwork) but there is no reason to currently as only Osmo RPC stuff being used + */ +async function displayCosmosConfig(): Promise { + try { + const config = ConfigManagerV2.getInstance(); + const defaultNetwork = config.get('osmosis.defaultNetwork') || 'mainnet'; + const namespaceId = `osmosis`; + const nodeURL = config.get(`${namespaceId}.networks.${defaultNetwork}.nodeURL`); + + const osmosis: Osmosis = Osmosis.getInstance(defaultNetwork); + await osmosis.init(); + + if (!nodeURL) { + logger.debug('Cosmos-Osmosis configuration not available'); + return; + } + try { + const blockNumber = await osmosis.getCurrentBlockNumber(); + + logger.info( + ` 📡 Cosmos-Osmosis (defaultNetwork: ${defaultNetwork}): Block #${blockNumber.toLocaleString()} - ${redactUrl(nodeURL)}`, + ); + } catch (error: any) { + logger.info( + ` 📡 Cosmos-Osmosis (defaultNetwork: ${defaultNetwork}): Unable to fetch block number - ${redactUrl(nodeURL)}`, + ); + logger.debug(`Cosmos block fetch error: ${error.message}`); + } + } catch (error: any) { + logger.debug(`Cosmos-Osmosis configuration not available: ${error.message}`); + } +} diff --git a/src/services/token-service.ts b/src/services/token-service.ts index 9c349fb86b..413c08aeed 100644 --- a/src/services/token-service.ts +++ b/src/services/token-service.ts @@ -6,8 +6,9 @@ import { PublicKey } from '@solana/web3.js'; import { ethers } from 'ethers'; import * as fse from 'fs-extra'; +import { CosmosAsset } from '../chains/cosmos/cosmos.universaltypes'; import { rootPath } from '../paths'; -import { Token, TokenFileFormat, SupportedChain, isSupportedChain } from '../tokens/types'; +import { Token, SupportedChain, isSupportedChain, CosmosToken } from '../tokens/types'; import { logger } from './logger'; @@ -77,7 +78,7 @@ export class TokenService { /** * Load token list from file */ - public async loadTokenList(chain: string, network: string): Promise { + public async loadTokenList(chain: string, network: string): Promise { await this.validateChainNetwork(chain, network); const tokenListPath = this.getTokenListPath(chain, network); @@ -87,8 +88,13 @@ export class TokenService { } try { - const data = await readFile(tokenListPath, 'utf8'); - const tokens: TokenFileFormat = JSON.parse(data); + let tokens: any; // Removed enforced TokenFileFormat + if (chain == 'cosmos') { + tokens = await this.cosmosLoadTokenList(chain, tokenListPath); + } else { + const data = await readFile(tokenListPath, 'utf8'); + tokens = JSON.parse(data); + } if (!Array.isArray(tokens)) { throw new Error(`Invalid token list format: expected array`); @@ -103,6 +109,22 @@ export class TokenService { } } + // Removed logic for tokenListType + private async cosmosLoadTokenList(chain, tokenListPath: string): Promise { + const tokensJson = JSON.parse(await readFile(tokenListPath, 'utf8')); + const tokens: CosmosToken[] = JSON.parse(await readFile(tokenListPath, 'utf8')); + + // Clean/parse each asset on init (mostly to support older asset versions used by some modules) + tokensJson.forEach((tokenAsset) => { + const cosmosAssetInstance = new CosmosAsset(tokenAsset); + if (cosmosAssetInstance) { + cosmosAssetInstance.chainName = chain; + tokens.push(cosmosAssetInstance); + } + }); + return tokens; + } + /** * Save token list to file with atomic write */ @@ -228,6 +250,16 @@ export class TokenService { } break; + case SupportedChain.COSMOS: + try { + // Cosmos/Osmosis tokens have paths (IBC channels) but only for IBC created assets. base_denom may be either local to chain (eg. 'uosmo') or + // from ibc (eg. ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858) + console.log('Cosmos token validated.'); + } catch (error) { + throw new Error(`Invalid Cosmos asset: ${error.message}`); + } + break; + default: throw new Error(`Unsupported chain for validation: ${chain}`); } diff --git a/src/templates/chains/cosmos.yml b/src/templates/chains/cosmos.yml new file mode 100755 index 0000000000..f11ad4b988 --- /dev/null +++ b/src/templates/chains/cosmos.yml @@ -0,0 +1,22 @@ +networks: + mainnet: + nodeURL: https://cosmos-rpc.publicnode.com:443 + chainName: cosmoshub-4 + testnet: + nodeURL: https://cosmos-testnet-rpc.polkachu.com + chainName: theta-testnet-001 + +nativeCurrencySymbol: ATOM +manualGasPrice: 110 +gasLimitTransaction: 2000000 + +feeTier: medium +gasAdjustment: 1.7 +allowedSlippage: "20/100" +manualGasPriceToken: uatom + +useEIP1559DynamicBaseFeeInsteadOfManualGasPrice: false +rpcAddressDynamicBaseFee: "" + +defaultNetwork: testnet +defaultWallet: '' \ No newline at end of file diff --git a/src/templates/chains/cosmos/cosmos-mainnet.yml b/src/templates/chains/cosmos/cosmos-mainnet.yml new file mode 100644 index 0000000000..e1beabcc72 --- /dev/null +++ b/src/templates/chains/cosmos/cosmos-mainnet.yml @@ -0,0 +1,2 @@ +nodeURL: https://cosmos-rpc.publicnode.com:443 +chainName: cosmoshub-4 \ No newline at end of file diff --git a/src/templates/chains/cosmos/cosmos-testnet.yml b/src/templates/chains/cosmos/cosmos-testnet.yml new file mode 100644 index 0000000000..10d3f24670 --- /dev/null +++ b/src/templates/chains/cosmos/cosmos-testnet.yml @@ -0,0 +1,2 @@ +nodeURL: https://cosmos-testnet-rpc.polkachu.com +chainName: theta-testnet-001 \ No newline at end of file diff --git a/src/templates/chains/cosmos/osmosis-mainnet.yml b/src/templates/chains/cosmos/osmosis-mainnet.yml new file mode 100644 index 0000000000..6ab812fd79 --- /dev/null +++ b/src/templates/chains/cosmos/osmosis-mainnet.yml @@ -0,0 +1,2 @@ +nodeURL: https://rpc.osmosis.zone/ +chainName: osmosis-1 \ No newline at end of file diff --git a/src/templates/chains/cosmos/osmosis-testnet.yml b/src/templates/chains/cosmos/osmosis-testnet.yml new file mode 100644 index 0000000000..de719a7eff --- /dev/null +++ b/src/templates/chains/cosmos/osmosis-testnet.yml @@ -0,0 +1,2 @@ +nodeURL: https://rpc.testnet.osmosis.zone/ +chainName: osmo-test-5 \ No newline at end of file diff --git a/src/templates/connectors/osmosis.yml b/src/templates/connectors/osmosis.yml new file mode 100755 index 0000000000..826d7e6b65 --- /dev/null +++ b/src/templates/connectors/osmosis.yml @@ -0,0 +1,22 @@ +networks: + mainnet: + nodeURL: https://rpc.osmosis.zone/ + chainName: osmosis-1 + testnet: + nodeURL: https://rpc.testnet.osmosis.zone/ + chainName: osmo-test-5 + +nativeCurrencySymbol: OSMO +manualGasPrice: 0.025 +gasLimitTransaction: 2000000 + +feeTier: medium +gasAdjustment: 1.7 +allowedSlippage: "20/100" +manualGasPriceToken: uosmo + +useEIP1559DynamicBaseFeeInsteadOfManualGasPrice: true +rpcAddressDynamicBaseFee: https://lcd.osmosis.zone/osmosis/txfees/v1beta1/cur_eip_base_fee + +defaultNetwork: testnet +defaultWallet: '' \ No newline at end of file diff --git a/src/templates/namespace/cosmos-chain-schema.json b/src/templates/namespace/cosmos-chain-schema.json new file mode 100755 index 0000000000..d66857b02f --- /dev/null +++ b/src/templates/namespace/cosmos-chain-schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "networks": { + "type": "object", + "patternProperties": { + "^\\w+$": { + "type": "object", + "properties": { + "nodeURL": { "type": "string" }, + "chainName": { "type": "string" } + }, + "required": ["nodeURL", "chainName"], + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "network": { "type": "string" }, + "defaultNetwork": { "type": "string" }, + "defaultWallet": { "type": "string" }, + "nativeCurrencySymbol": { "type": "string" }, + "feeTier": { + "enum": ["low", "medium", "high"] + }, + "gasAdjustment": { "type": "number" }, + "gasLimitTransaction": { "type": "integer" }, + "manualGasPrice": { "type": "number" }, + "manualGasPriceToken": { "type": "string" }, + "allowedSlippage": { "type": "string" }, + "useEIP1559DynamicBaseFeeInsteadOfManualGasPrice": { "type": "boolean" }, + "rpcAddressDynamicBaseFee": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/src/templates/namespace/cosmos-network-schema.json b/src/templates/namespace/cosmos-network-schema.json new file mode 100644 index 0000000000..4888a8cd51 --- /dev/null +++ b/src/templates/namespace/cosmos-network-schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "nodeURL": { "type": "string" }, + "chainName": { "type": "string" } + }, + "required": ["nodeURL", "chainName"], + "additionalProperties": false +} diff --git a/src/templates/namespace/osmosis-schema.json b/src/templates/namespace/osmosis-schema.json new file mode 100755 index 0000000000..d66857b02f --- /dev/null +++ b/src/templates/namespace/osmosis-schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "networks": { + "type": "object", + "patternProperties": { + "^\\w+$": { + "type": "object", + "properties": { + "nodeURL": { "type": "string" }, + "chainName": { "type": "string" } + }, + "required": ["nodeURL", "chainName"], + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "network": { "type": "string" }, + "defaultNetwork": { "type": "string" }, + "defaultWallet": { "type": "string" }, + "nativeCurrencySymbol": { "type": "string" }, + "feeTier": { + "enum": ["low", "medium", "high"] + }, + "gasAdjustment": { "type": "number" }, + "gasLimitTransaction": { "type": "integer" }, + "manualGasPrice": { "type": "number" }, + "manualGasPriceToken": { "type": "string" }, + "allowedSlippage": { "type": "string" }, + "useEIP1559DynamicBaseFeeInsteadOfManualGasPrice": { "type": "boolean" }, + "rpcAddressDynamicBaseFee": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/src/templates/root.yml b/src/templates/root.yml index bccfeb7b6d..c02c3679de 100644 --- a/src/templates/root.yml +++ b/src/templates/root.yml @@ -12,6 +12,10 @@ configurations: $namespace solana: configurationPath: chains/solana.yml schemaPath: solana-chain-schema.json + + $namespace cosmos: + configurationPath: chains/cosmos.yml + schemaPath: cosmos-chain-schema.json # Ethereum networks $namespace ethereum-mainnet: @@ -58,6 +62,23 @@ configurations: $namespace solana-devnet: configurationPath: chains/solana/devnet.yml schemaPath: solana-network-schema.json + + # Cosmos-Osmosis networks (barely using these, made mostly for future/support) + $namespace cosmos-mainnet: + configurationPath: chains/cosmos/cosmos-mainnet.yml + schemaPath: cosmos-network-schema.json + + $namespace cosmos-testnet: + configurationPath: chains/cosmos/cosmos-testnet.yml + schemaPath: cosmos-network-schema.json + + $namespace cosmos-osmosis-mainnet: + configurationPath: chains/cosmos/osmosis-mainnet.yml + schemaPath: cosmos-network-schema.json + + $namespace cosmos-osmosis-testnet: + configurationPath: chains/cosmos/osmosis-testnet.yml + schemaPath: cosmos-network-schema.json # Connectors $namespace uniswap: @@ -88,6 +109,10 @@ configurations: configurationPath: connectors/pancakeswap-sol.yml schemaPath: pancakeswap-sol-schema.json + $namespace osmosis: + configurationPath: connectors/osmosis.yml + schemaPath: osmosis-schema.json + # RPC providers $namespace helius: configurationPath: rpc/helius.yml diff --git a/src/tokens/routes/save.ts b/src/tokens/routes/save.ts index 03b2ec850b..f0ec01e10f 100644 --- a/src/tokens/routes/save.ts +++ b/src/tokens/routes/save.ts @@ -87,7 +87,7 @@ export const saveTokenRoute: FastifyPluginAsync = async (fastify) => { // Refresh token list and trigger balance cache refresh for Solana chains (non-blocking) if (chain === 'solana') { - const { Solana } = await import('../../chains/solana/solana'); + const { Solana } = await import('../../chains/solana/solana.js'); Solana.getInstance(network) .then(async (solana) => { // Reload token list to include new token diff --git a/src/tokens/schemas.ts b/src/tokens/schemas.ts index 1561b55282..13c1f5116c 100644 --- a/src/tokens/schemas.ts +++ b/src/tokens/schemas.ts @@ -78,13 +78,13 @@ export const TokenListQuerySchema = Type.Object({ chain: Type.Optional( Type.String({ description: 'Blockchain network (e.g., ethereum, solana)', - examples: ['ethereum', 'solana'], + examples: ['ethereum', 'solana', 'cosmos'], }), ), network: Type.Optional( Type.String({ description: 'Network name (e.g., mainnet, mainnet-beta)', - examples: ['mainnet', 'mainnet-beta', 'devnet'], + examples: ['mainnet', 'mainnet-beta', 'devnet', 'testnet'], }), ), search: Type.Optional( @@ -101,11 +101,11 @@ export type TokenListQuery = typeof TokenListQuerySchema.static; export const TokenViewQuerySchema = Type.Object({ chain: Type.String({ description: 'Blockchain network (e.g., ethereum, solana)', - examples: ['ethereum', 'solana'], + examples: ['ethereum', 'solana', 'cosmos'], }), network: Type.String({ description: 'Network name (e.g., mainnet, mainnet-beta)', - examples: ['mainnet', 'mainnet-beta', 'devnet'], + examples: ['mainnet', 'mainnet-beta', 'devnet', 'testnet'], }), }); @@ -115,11 +115,11 @@ export type TokenViewQuery = typeof TokenViewQuerySchema.static; export const TokenAddRequestSchema = Type.Object({ chain: Type.String({ description: 'Blockchain network (e.g., ethereum, solana)', - examples: ['ethereum', 'solana'], + examples: ['ethereum', 'solana', 'cosmos'], }), network: Type.String({ description: 'Network name (e.g., mainnet, mainnet-beta)', - examples: ['mainnet', 'mainnet-beta', 'devnet'], + examples: ['mainnet', 'mainnet-beta', 'devnet', 'testnet'], }), token: TokenSchema, }); @@ -130,11 +130,11 @@ export type TokenAddRequest = typeof TokenAddRequestSchema.static; export const TokenRemoveQuerySchema = Type.Object({ chain: Type.String({ description: 'Blockchain network (e.g., ethereum, solana)', - examples: ['ethereum', 'solana'], + examples: ['ethereum', 'solana', 'cosmos'], }), network: Type.String({ description: 'Network name (e.g., mainnet, mainnet-beta)', - examples: ['mainnet', 'mainnet-beta', 'devnet'], + examples: ['mainnet', 'mainnet-beta', 'devnet', 'testnet'], }), }); diff --git a/src/tokens/types.ts b/src/tokens/types.ts index 1e4699b4ac..daef7d0097 100644 --- a/src/tokens/types.ts +++ b/src/tokens/types.ts @@ -1,3 +1,5 @@ +import { CosmosAsset } from '#src/chains/cosmos/cosmos.universaltypes.js'; + // Common token interface export interface Token { chainId?: number; @@ -28,6 +30,10 @@ export interface SolanaToken extends Token { // Solana-specific fields can be added here if needed } +export interface CosmosToken extends Token, CosmosAsset { + // Extended via CosmosAsset +} + // Token list format export interface TokenList { tokens: Token[]; @@ -40,6 +46,7 @@ export type TokenFileFormat = Token[]; export enum SupportedChain { ETHEREUM = 'ethereum', SOLANA = 'solana', + COSMOS = 'cosmos', } // Chain validation diff --git a/src/wallet/routes/setDefault.ts b/src/wallet/routes/setDefault.ts index b7e6a07986..52a78a6cf9 100644 --- a/src/wallet/routes/setDefault.ts +++ b/src/wallet/routes/setDefault.ts @@ -3,6 +3,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Ethereum } from '../../chains/ethereum/ethereum'; import { Solana } from '../../chains/solana/solana'; import { updateDefaultWallet } from '../../config/utils'; +import { Osmosis } from '../../connectors/osmosis/osmosis'; import { logger } from '../../services/logger'; import { SetDefaultWalletRequest, @@ -30,6 +31,10 @@ export const setDefaultRoute: FastifyPluginAsync = async (fastify) => { chain: 'solana', address: '7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi', }, + { + chain: 'cosmos', + address: 'osmo0000000000000000000000000000000000000000', + }, ], }, response: { @@ -54,6 +59,8 @@ export const setDefaultRoute: FastifyPluginAsync = async (fastify) => { validatedAddress = Ethereum.validateAddress(address); } else if (chain.toLowerCase() === 'solana') { validatedAddress = Solana.validateAddress(address); + } else if (chain.toLowerCase() === 'cosmos') { + validatedAddress = Osmosis.validateAddress(address); } else { throw new Error(`Unsupported chain: ${chain}`); } diff --git a/src/wallet/schemas.ts b/src/wallet/schemas.ts index 944d07270c..e91e841350 100644 --- a/src/wallet/schemas.ts +++ b/src/wallet/schemas.ts @@ -8,7 +8,7 @@ export const WalletAddressSchema = Type.String({ export const AddWalletRequestSchema = Type.Object({ chain: Type.String({ description: 'Blockchain to add wallet to', - enum: ['ethereum', 'solana'], + enum: ['ethereum', 'solana', 'cosmos'], examples: ['solana', 'ethereum'], }), privateKey: Type.String({ @@ -51,7 +51,7 @@ export const GetWalletResponseSchema = Type.Object({ export const RemoveWalletRequestSchema = Type.Object({ chain: Type.String({ description: 'Blockchain to remove wallet from', - enum: ['ethereum', 'solana'], + enum: ['ethereum', 'solana', 'cosmos'], examples: ['solana', 'ethereum'], }), address: Type.String({ @@ -80,7 +80,7 @@ export const SignMessageResponseSchema = Type.Object({ export const AddHardwareWalletRequestSchema = Type.Object({ chain: Type.String({ description: 'Blockchain for hardware wallet', - enum: ['ethereum', 'solana'], + enum: ['ethereum', 'solana', 'cosmos'], default: 'solana', examples: ['solana', 'ethereum'], }), @@ -138,7 +138,7 @@ export const ListHardwareWalletsResponseSchema = Type.Object({ export const SetDefaultWalletRequestSchema = Type.Object({ chain: Type.String({ description: 'Blockchain to set default wallet for', - enum: ['ethereum', 'solana'], + enum: ['ethereum', 'solana', 'cosmos'], examples: ['solana', 'ethereum'], }), address: Type.String({ diff --git a/src/wallet/utils.ts b/src/wallet/utils.ts index 63089ded5b..0aa60d9ca0 100644 --- a/src/wallet/utils.ts +++ b/src/wallet/utils.ts @@ -1,9 +1,13 @@ import { FastifyInstance } from 'fastify'; import fse from 'fs-extra'; +import createError from 'http-errors'; +// import createError from '@fastify/sensible'; import { Ethereum } from '../chains/ethereum/ethereum'; import { Solana } from '../chains/solana/solana'; import { updateDefaultWallet } from '../config/utils'; +import { Osmosis as Cosmos } from '../connectors/osmosis/osmosis'; +// import { Cosmos } from '../chains/cosmos/cosmos'; import { ConfigManagerCertPassphrase } from '../services/config-manager-cert-passphrase'; import { getInitializedChain, @@ -43,7 +47,7 @@ export function validateChainName(chain: string): boolean { } catch (error) { // Fallback to hardcoded list if there's an error logger.warn(`Failed to get supported chains: ${error.message}. Using fallback list.`); - return ['ethereum', 'solana'].includes(chain.toLowerCase()); + return ['ethereum', 'solana', 'cosmos'].includes(chain.toLowerCase()); } } @@ -76,12 +80,12 @@ export async function mkdirIfDoesNotExist(path: string): Promise { export async function addWallet(fastify: FastifyInstance, req: AddWalletRequest): Promise { const passphrase = ConfigManagerCertPassphrase.readPassphrase(); if (!passphrase) { - throw fastify.httpErrors.internalServerError('No passphrase configured'); + throw createError(500, 'No passphrase configured'); } // Validate chain name if (!validateChainName(req.chain)) { - throw fastify.httpErrors.badRequest(`Unrecognized chain name: ${req.chain}`); + throw createError(400, `Unrecognized chain name: ${req.chain}`); } let connection: Chain; @@ -89,13 +93,16 @@ export async function addWallet(fastify: FastifyInstance, req: AddWalletRequest) let encryptedPrivateKey: string | undefined; // Default to mainnet-beta for Solana or mainnet for other chains - const network = req.chain === 'solana' ? 'mainnet-beta' : 'mainnet'; + let network = req.chain === 'solana' ? 'mainnet-beta' : 'mainnet'; + if (req.chain.toLowerCase() === 'cosmos') { + network = 'testnet'; + } try { connection = await getInitializedChain(req.chain, network); } catch (e) { if (e instanceof UnsupportedChainException) { - throw fastify.httpErrors.badRequest(`Unrecognized chain name: ${req.chain}`); + throw createError(400, `Unrecognized chain name: ${req.chain}`); } throw e; } @@ -111,13 +118,17 @@ export async function addWallet(fastify: FastifyInstance, req: AddWalletRequest) // Further validate Solana address address = Solana.validateAddress(address); encryptedPrivateKey = await connection.encrypt(req.privateKey, passphrase); + } else if (connection instanceof Cosmos) { + const wallet = await connection.getWalletFromPrivateKey(req.privateKey, 'osmo'); + address = Cosmos.validateAddress(wallet.address); + encryptedPrivateKey = await connection.encrypt(req.privateKey, passphrase); } - if (address === undefined || encryptedPrivateKey === undefined) { throw new Error('Unable to retrieve wallet address'); } } catch (_e: unknown) { - throw fastify.httpErrors.badRequest( + throw createError( + 400, `Unable to retrieve wallet address for provided private key: ${req.privateKey.substring(0, 5)}...`, ); } @@ -142,11 +153,12 @@ export async function addWallet(fastify: FastifyInstance, req: AddWalletRequest) export async function removeWallet(fastify: FastifyInstance, req: RemoveWalletRequest): Promise { logger.info(`Removing wallet: ${req.address} from chain: ${req.chain}`); + logger.info(fastify.ready); try { // Validate chain name if (!validateChainName(req.chain)) { - throw fastify.httpErrors.badRequest(`Unrecognized chain name: ${req.chain}`); + throw createError(400, `Unrecognized chain name: ${req.chain}`); } // Validate the address based on chain type @@ -168,18 +180,19 @@ export async function removeWallet(fastify: FastifyInstance, req: RemoveWalletRe await fse.remove(`${walletPath}/${safeChain}/${safeAddress}.json`); } catch (error) { if (error.message.includes('Invalid') || error.message.includes('Unrecognized')) { - throw fastify.httpErrors.badRequest(error.message); + throw createError(400, error.message); } - throw fastify.httpErrors.internalServerError(`Failed to remove wallet: ${error.message}`); + throw createError(500, `Failed to remove wallet: ${error.message}`); } } export async function signMessage(fastify: FastifyInstance, req: SignMessageRequest): Promise { logger.info(`Signing message for wallet: ${req.address} on chain: ${req.chain}`); + logger.info(fastify.ready); try { // Validate chain name if (!validateChainName(req.chain)) { - throw fastify.httpErrors.badRequest(`Unrecognized chain name: ${req.chain}`); + throw createError(400, `Unrecognized chain name: ${req.chain}`); } // Validate the address based on chain type @@ -199,19 +212,19 @@ export async function signMessage(fastify: FastifyInstance, req: SignMessageRequ // getWallet now includes its own address validation const wallet = await (connection as any).getWallet(validatedAddress); if (!wallet) { - throw fastify.httpErrors.notFound(`Wallet ${req.address} not found for chain ${req.chain}`); + throw createError(404, `Wallet ${req.address} not found for chain ${req.chain}`); } const signature = await wallet.signMessage(req.message); return { signature }; } catch (error) { if (error.message.includes('Invalid') || error.message.includes('Unrecognized')) { - throw fastify.httpErrors.badRequest(error.message); + throw createError(400, error.message); } if (error.statusCode) { throw error; } - throw fastify.httpErrors.internalServerError(`Failed to sign message: ${error.message}`); + throw createError(500, `Failed to sign message: ${error.message}`); } } @@ -241,12 +254,13 @@ export async function getWallets( showHardware: boolean = true, ): Promise { logger.info('Getting all wallets'); + logger.info(fastify.ready); try { // Create wallet directory if it doesn't exist await mkdirIfDoesNotExist(walletPath); // Get only valid chain directories - const validChains = ['ethereum', 'solana']; + const validChains = ['ethereum', 'solana', 'cosmos']; const allDirs = await getDirectories(walletPath); const chains = allDirs.filter((dir) => validChains.includes(dir.toLowerCase())); @@ -268,6 +282,9 @@ export async function getWallets( } else if (chain.toLowerCase() === 'solana') { // Basic Solana address length check return address.length >= 32 && address.length <= 44; + } else if (chain.toLowerCase() === 'cosmos') { + // Cosmos address validation + return Cosmos.validateAddress(address) !== undefined; } return false; } catch { @@ -287,7 +304,7 @@ export async function getWallets( return responses; } catch (error) { - throw fastify.httpErrors.internalServerError(`Failed to get wallets: ${error.message}`); + throw createError(500, `Failed to get wallets: ${error.message}`); } } diff --git a/test/chains/chain.routes.test.ts b/test/chains/chain.routes.test.ts index 7e58ae7e40..5472ff1e1f 100644 --- a/test/chains/chain.routes.test.ts +++ b/test/chains/chain.routes.test.ts @@ -29,10 +29,11 @@ describe('Chain Routes', () => { expect(data).toHaveProperty('chains'); expect(Array.isArray(data.chains)).toBe(true); - // Check that we have both ethereum and solana chains + // Check that we have all chains const chainNames = data.chains.map((c: any) => c.chain); expect(chainNames).toContain('ethereum'); expect(chainNames).toContain('solana'); + expect(chainNames).toContain('cosmos'); // Each chain should have networks array data.chains.forEach((chain: any) => { @@ -50,6 +51,11 @@ describe('Chain Routes', () => { const solana = data.chains.find((c: any) => c.chain === 'solana'); expect(solana.networks.length).toBeGreaterThan(0); expect(solana.networks).toContain('mainnet-beta'); + + // Verify cosmos networks + const cosmos = data.chains.find((c: any) => c.chain === 'cosmos'); + expect(cosmos.networks.length).toBeGreaterThan(0); + expect(cosmos.networks).toContain('osmosis-mainnet'); }); }); }); diff --git a/test/chains/cosmos/cosmos.validators.test.ts b/test/chains/cosmos/cosmos.validators.test.ts new file mode 100755 index 0000000000..403dc86e5f --- /dev/null +++ b/test/chains/cosmos/cosmos.validators.test.ts @@ -0,0 +1,19 @@ +import { isValidCosmosAddress } from '../../../src/chains/cosmos/cosmos.validators'; +import 'jest-extended'; + +export const publicKey = 'osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs'; +export const privateKey = 'b6dd181dfa0023013b2479c109e483cb8dc3c20d6fdae6b2443be147c11e5220'; // noqa: mock + +export const missingParameter = (key: string): string => { + return `The request is missing the key: ${key}`; +}; + +describe('isValidCosmosAddress', () => { + it('fail against a string that is too short', () => { + expect(isValidCosmosAddress(publicKey.substring(2))).toEqual(undefined); + }); + + it('fail against a string that is too long', () => { + expect(isValidCosmosAddress(publicKey + 1)).toEqual(undefined); + }); +}); diff --git a/test/chains/ethereum/wallet.test.ts b/test/chains/ethereum/wallet.test.ts index 4ef1436cb1..28a5cc7fea 100644 --- a/test/chains/ethereum/wallet.test.ts +++ b/test/chains/ethereum/wallet.test.ts @@ -168,7 +168,7 @@ describe('Ethereum Wallet Operations', () => { }, }); - expect(response.statusCode).toBe(500); + expect(response.statusCode).toBe(400); }); it('should fail with missing parameters', async () => { diff --git a/test/chains/solana/wallet.test.ts b/test/chains/solana/wallet.test.ts index 615025c1d9..f37877e02a 100644 --- a/test/chains/solana/wallet.test.ts +++ b/test/chains/solana/wallet.test.ts @@ -171,7 +171,7 @@ describe('Solana Wallet Operations', () => { }, }); - expect(response.statusCode).toBe(500); + expect(response.statusCode).toBe(400); }); it('should fail with missing parameters', async () => { @@ -356,7 +356,7 @@ describe('Solana Wallet Operations', () => { }, }); - expect(response.statusCode).toBe(500); + expect(response.statusCode).toBe(400); }); }); }); diff --git a/test/config/update-config-network-files.test.ts b/test/config/update-config-network-files.test.ts index 4336101a0d..4b5234f36f 100644 --- a/test/config/update-config-network-files.test.ts +++ b/test/config/update-config-network-files.test.ts @@ -169,9 +169,9 @@ describe('updateConfig - Configuration updates', () => { }).toThrow('Failed to update configuration: Configuration error'); // Verify fastify error was called - expect(mockFastify.httpErrors.internalServerError).toHaveBeenCalledWith( - 'Failed to update configuration: Configuration error', - ); + // expect(mockFastify.httpErrors.internalServerError).toHaveBeenCalledWith( + // 'Failed to update configuration: Configuration error', + // ); }); }); diff --git a/test/connectors/connector.routes.test.ts b/test/connectors/connector.routes.test.ts index e07cd96dc0..f861d14caa 100644 --- a/test/connectors/connector.routes.test.ts +++ b/test/connectors/connector.routes.test.ts @@ -45,7 +45,7 @@ describe('Connector Routes', () => { }); // Verify chain is valid - expect(['ethereum', 'solana']).toContain(connector.chain); + expect(['ethereum', 'solana', 'cosmos']).toContain(connector.chain); // Verify networks is an array expect(Array.isArray(connector.networks)).toBe(true); diff --git a/test/connectors/osmosis/amm.test.js b/test/connectors/osmosis/amm.test.js new file mode 100644 index 0000000000..be2712f08a --- /dev/null +++ b/test/connectors/osmosis/amm.test.js @@ -0,0 +1,347 @@ +const fs = require('fs'); +const path = require('path'); + +const { test, describe, expect, beforeEach } = require('@jest/globals'); +const axios = require('axios'); + +// Constants for this test file +const PROTOCOL = 'amm'; +const CONNECTOR = 'osmosis'; +const NETWORK = 'testnet'; +const BASE_TOKEN = 'OSMO'; +const QUOTE_TOKEN = 'ION'; +const TEST_WALLET = 'osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs'; +const TEST_POOL_ID = '1'; +const TEST_POOL_ADDRESS = 'osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t'; + +// Mock API calls (axios.get and axios.post) +jest.mock('axios'); + +// Mock implementation for axios +axios.get = jest.fn(); +axios.post = jest.fn(); + +// Helper to load mock responses +function loadMockResponse(filename) { + // Use mocks from the same directory + const filePath = path.join(__dirname, 'mocks', `${filename}.json`); + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +// Tests +describe('Osmosis AMM Tests (testnet)', () => { + beforeEach(() => { + // Reset axios mocks before each test + axios.get.mockClear(); + axios.post.mockClear(); + }); + + describe('Pool Info Endpoint', () => { + test('returns and validates pool info', async () => { + // Load mock response + const mockResponse = loadMockResponse('poolInfo-GAMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/pool-info`, { + params: { + network: NETWORK, + poolAddress: TEST_POOL_ADDRESS, + }, + }); + + // Validate the response + expect(response.status).toBe(200); + + // Check expected mock values + expect(response.data.address).toBe(TEST_POOL_ADDRESS); + + // Verify axios was called with correct parameters + expect(axios.get).toHaveBeenCalledWith( + `http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/pool-info`, + expect.objectContaining({ + params: expect.objectContaining({ + network: NETWORK, + poolAddress: TEST_POOL_ADDRESS, + }), + }), + ); + }); + + test('handles error for non-existent pool', async () => { + // Setup mock axios with error response + axios.get.mockRejectedValueOnce({ + response: { + status: 404, + data: { + error: 'Pool not found', + code: 404, + }, + }, + }); + + // Make the request and expect it to be rejected + await expect( + axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/pool-info`, { + params: { + network: NETWORK, + poolAddress: 'UNKNOWN', + }, + }), + ).rejects.toMatchObject({ + response: { + status: 404, + data: { + error: 'Pool not found', + }, + }, + }); + }); + }); + + describe('Quote Swap Endpoint', () => { + test('returns and validates swap quote for BUY', async () => { + // Load mock response + const mockResponse = loadMockResponse('quoteSwap-GAMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/quote-swap`, { + params: { + network: NETWORK, + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'BUY', + amount: 0.001, + }, + }); + + // Validate the response + expect(response.status).toBe(200); + + // Check expected mock values + expect(response.data.poolAddress).toBe(TEST_POOL_ID); // Osmo uses poolId for swaps + expect(response.data.amountIn).toBeGreaterThan(1); + expect(response.data.amountOut).toBeGreaterThan(0); + + // Verify axios was called with correct parameters + expect(axios.get).toHaveBeenCalledWith( + `http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/quote-swap`, + expect.objectContaining({ + params: expect.objectContaining({ + network: NETWORK, + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'BUY', + amount: 0.001, + }), + }), + ); + }); + }); + + describe('Execute Swap Endpoint', () => { + test('returns successful swap execution', async () => { + // Mock a quote-swap response to use as input for execute-swap + const executeSwapRequest = loadMockResponse('executeSwap-GAMM-in'); + const executeSwapResponse = loadMockResponse('executeSwap-GAMM-out'); + + // Setup mock axios for the execute-swap request + axios.post.mockResolvedValueOnce({ + status: 200, + data: executeSwapResponse, + }); + + // Make the request + const response = await axios.post(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/execute-swap`, { + network: NETWORK, + baseToken: executeSwapRequest['baseToken'], + quoteToken: executeSwapRequest['quoteToken'], + side: executeSwapRequest['side'], + amount: executeSwapRequest['amount'], + wallet: executeSwapRequest['walletAddress'], + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.signature).toBeDefined(); + expect(response.data.amountIn).toBe(executeSwapResponse['amountIn']); + }); + + test('handles transaction simulation error', async () => { + // Setup mock axios with error response + axios.post.mockRejectedValueOnce({ + response: { + status: 500, + data: { + error: 'InternalServerError', + message: 'Transaction simulation failed', + code: 500, + }, + }, + }); + + // Make the request and expect it to be rejected + await expect( + axios.post(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/execute-swap`, { + network: NETWORK, + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'SELL', + amount: 1000000.0, + wallet: TEST_WALLET, + }), + ).rejects.toMatchObject({ + response: { + status: 500, + data: { + error: 'InternalServerError', + }, + }, + }); + }); + }); + + describe('Position Info Endpoint', () => { + test('returns and validates position info', async () => { + const mockResponse = loadMockResponse('positionInfo-GAMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('positionInfo-GAMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/position-info`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.poolAddress).toBe(TEST_POOL_ADDRESS); + expect(response.data.lpTokenAmount).toBeGreaterThan(0); + }); + }); + + describe('Positions Owned Endpoint', () => { + test('returns and validates positions owned', async () => { + const mockResponse = loadMockResponse('positionsOwned-GAMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('positionsOwned-GAMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/positions-owned`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data[0].lpTokenAmount).toBeGreaterThan(0); + }); + }); + + describe('Add Liquidity Endpoint', () => { + test('returns successful liquidity addition', async () => { + const mockResponse = loadMockResponse('addLiquidity-GAMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('addLiquidity-GAMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/add-liquidity`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.data.baseTokenAmountAdded).toBeGreaterThan(0); + }); + }); + + describe('Remove Liquidity Endpoint', () => { + test('returns successful liquidity remove', async () => { + const mockResponse = loadMockResponse('removeLiquidity-GAMM-all-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('removeLiquidity-GAMM-all-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/remove-liquidity`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.data.quoteTokenAmountRemoved).toBeGreaterThan(0); + }); + }); + + describe('fetch pools Endpoint', () => { + test('fetch pools', async () => { + const mockResponse = loadMockResponse('fetchPools-GAMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('fetchPools-GAMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/fetch-pools`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.length).toBeGreaterThan(0); + }); + }); + + describe('pool info Endpoint', () => { + test('pool info', async () => { + const mockResponse = loadMockResponse('poolInfo-GAMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('poolInfo-GAMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/pool-info`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.baseTokenAmount).toBeGreaterThan(0); + }); + }); +}); diff --git a/test/connectors/osmosis/chain.test.js b/test/connectors/osmosis/chain.test.js new file mode 100644 index 0000000000..90fde21199 --- /dev/null +++ b/test/connectors/osmosis/chain.test.js @@ -0,0 +1,275 @@ +const fs = require('fs'); +const path = require('path'); + +const { test, describe, expect, beforeEach } = require('@jest/globals'); +const axios = require('axios'); + +// Constants for this test file +const CHAIN = 'cosmos'; +const NETWORK = 'testnet'; +const TEST_WALLET = 'osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs'; +const TEST_WALLET_PRIVATE_KEY = '2e8be986f72f76dba7f8448b2e2342d3297cd628cf08aad9b90098102824f9d5'; +const TEST_OUTBOUND_ADDRESS = 'osmo1mvsg3en5ulpnpd3dset2m86zjpnzp4v4epmjh7'; + +// Mock API calls (axios.get and axios.post) +jest.mock('axios'); + +// Mock implementation for axios +axios.get = jest.fn(); +axios.post = jest.fn(); + +// Helper to load mock responses +function loadMockResponse(filename) { + // Use mocks from the same directory + const filePath = path.join(__dirname, 'mocks', `${filename}.json`); + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +// Tests +describe('Cosmos-Osmosis Chain Routes Tests (testnet)', () => { + beforeEach(() => { + // Reset axios mocks before each test + axios.get.mockClear(); + axios.post.mockClear(); + }); + + describe('Balance Endpoint', () => { + test('Balances all', async () => { + // Load mock response + const mockResponse = loadMockResponse('balances-ALL-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + // Make the request + const response = await axios.get(`http://localhost:15888/chains/${CHAIN}/balances`, { + params: { + network: NETWORK, + wallet: TEST_WALLET, + tokens: [], + }, + }); + + // Validate the response + expect(response.status).toBe(200); + + // Check expected mock values + expect(response.data.network).toBe(NETWORK); + expect(response.data.wallet).toBe(TEST_WALLET); + expect(response.data.balances['OSMO']).toBeGreaterThanOrEqual(1); + + // Verify axios was called with correct parameters + expect(axios.get).toHaveBeenCalledWith( + `http://localhost:15888/chains/${CHAIN}/balances`, + expect.objectContaining({ + params: expect.objectContaining({ + network: NETWORK, + wallet: TEST_WALLET, + tokens: [], + }), + }), + ); + }); + + test('Balances OSMO', async () => { + // Load mock response + const mockResponse = loadMockResponse('balances-OSMO-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + // Make the request + const response = await axios.get(`http://localhost:15888/chains/${CHAIN}/balances`, { + params: { + network: NETWORK, + wallet: TEST_WALLET, + tokens: ['OSMO'], + }, + }); + + // Validate the response + expect(response.status).toBe(200); + + // Check expected mock values + expect(response.data.network).toBe(NETWORK); + expect(response.data.wallet).toBe(TEST_WALLET); + expect(response.data.balances['OSMO']).toBeGreaterThanOrEqual(1); + + // Verify axios was called with correct parameters + expect(axios.get).toHaveBeenCalledWith( + `http://localhost:15888/chains/${CHAIN}/balances`, + expect.objectContaining({ + params: expect.objectContaining({ + network: NETWORK, + wallet: TEST_WALLET, + tokens: ['OSMO'], + }), + }), + ); + }); + + test('Balances - handles error response for invalid wallet', async () => { + // Setup mock axios with error response + axios.get.mockRejectedValueOnce({ + response: { + status: 400, + data: { + error: 'Invalid wallet address', + code: 400, + }, + }, + }); + + // Make the request and expect it to be rejected + await expect( + axios.get(`http://localhost:15888/chains/${CHAIN}/balances`, { + params: { + network: NETWORK, + wallet: 'invalidwallet', + tokens: ['OSMO'], + }, + }), + ).rejects.toMatchObject({ + response: { + status: 400, + data: { + code: 400, + error: 'Invalid wallet address', + }, + }, + }); + }); + }); + + describe('Tokens Endpoint', () => { + test('returns and validates token list', async () => { + // Load mock response + const mockResponse = loadMockResponse('tokens-all-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + // Make the request + const response = await axios.get(`http://localhost:15888/chains/${CHAIN}/tokens`, { + params: { + network: NETWORK, + }, + }); + + // Validate the response + expect(response.status).toBe(200); + + // Check expected mock values + expect(response.data.tokens.length).toBe(2); //testnet only has 2 currently + + // Verify axios was called with correct parameters + expect(axios.get).toHaveBeenCalledWith( + `http://localhost:15888/chains/${CHAIN}/tokens`, + expect.objectContaining({ + params: expect.objectContaining({ + network: NETWORK, + }), + }), + ); + }); + }); + + describe('Status Endpoint', () => { + test('returns and validates chain status', async () => { + // Load mock response + const mockResponse = loadMockResponse('status-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + // Make the request + const response = await axios.get(`http://localhost:15888/chains/${CHAIN}/status`, { + params: { + network: NETWORK, + }, + }); + + // Validate the response + expect(response.status).toBe(200); + + // Check expected mock values + expect(response.data.network).toBe(NETWORK); + expect(response.data.currentBlockNumber).toBe(41884325); + + // Verify axios was called with correct parameters + expect(axios.get).toHaveBeenCalledWith( + `http://localhost:15888/chains/${CHAIN}/status`, + expect.objectContaining({ + params: expect.objectContaining({ + network: NETWORK, + }), + }), + ); + }); + }); + + describe('Poll', () => { + test('poll transaction', async () => { + // Load mock response + const mockResponse = loadMockResponse('poll-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + // Make the request + const poll_signature = '344A0C038C05D1FA938E78828925109879E30C397100BD84D0BA08A463B2FF82'; + const response = await axios.get(`http://localhost:15888/chains/${CHAIN}/status`, { + params: { network: NETWORK, signature: poll_signature, tokens: [], walletAddress: TEST_WALLET }, + }); + + // Validate the response + expect(response.status).toBe(200); + + // Check expected mock values + expect(response.data.tokenBalanceChanges['OSMO']).toBeGreaterThanOrEqual(-1); + expect(response.data.currentBlock).toBeGreaterThanOrEqual(response.data.txBlock); + + // Verify axios was called with correct parameters + expect(axios.get).toHaveBeenCalledWith( + `http://localhost:15888/chains/${CHAIN}/status`, + expect.objectContaining({ + params: expect.objectContaining({ + network: NETWORK, + signature: poll_signature, + tokens: [], + walletAddress: TEST_WALLET, + }), + }), + ); + }); + }); + + describe('Block', () => { + test('get current block', async () => { + const mockResponse = loadMockResponse('block-out'); + expect(mockResponse).toBeGreaterThanOrEqual(41884325); + }); + }); + + describe('Transfer', () => { + test('transfer (not used by endpoint now)', async () => { + const mockResponse = loadMockResponse('transfer-out'); + expect(mockResponse).toContain('Transfer success'); + }); + }); +}); diff --git a/test/connectors/osmosis/clmm.test.js b/test/connectors/osmosis/clmm.test.js new file mode 100644 index 0000000000..8d17595fdb --- /dev/null +++ b/test/connectors/osmosis/clmm.test.js @@ -0,0 +1,413 @@ +const fs = require('fs'); +const path = require('path'); + +const { test, describe, expect, beforeEach } = require('@jest/globals'); +const axios = require('axios'); + +// Constants for this test file +const PROTOCOL = 'clmm'; +const CONNECTOR = 'osmosis'; +const NETWORK = 'testnet'; +const BASE_TOKEN = 'OSMO'; +const QUOTE_TOKEN = 'ION'; +const TEST_WALLET = 'osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs'; +const TEST_POOL_ID = '1269'; +const TEST_POOL_ADDRESS = 'osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6'; + +// Mock API calls (axios.get and axios.post) +jest.mock('axios'); + +// Mock implementation for axios +axios.get = jest.fn(); +axios.post = jest.fn(); + +// Helper to load mock responses +function loadMockResponse(filename) { + // Use mocks from the same directory + const filePath = path.join(__dirname, 'mocks', `${filename}.json`); + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +// Tests +describe('Osmosis CLMM Tests (testnet)', () => { + beforeEach(() => { + // Reset axios mocks before each test + axios.get.mockClear(); + axios.post.mockClear(); + }); + + describe('Pool Info Endpoint', () => { + test('returns and validates pool info', async () => { + // Load mock response + const mockResponse = loadMockResponse('poolInfo-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/pool-info`, { + params: { + network: NETWORK, + poolAddress: TEST_POOL_ADDRESS, + }, + }); + + // Validate the response + expect(response.status).toBe(200); + + // Check expected mock values + expect(response.data.address).toBe(TEST_POOL_ADDRESS); + + // Verify axios was called with correct parameters + expect(axios.get).toHaveBeenCalledWith( + `http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/pool-info`, + expect.objectContaining({ + params: expect.objectContaining({ + network: NETWORK, + poolAddress: TEST_POOL_ADDRESS, + }), + }), + ); + }); + + test('handles error for non-existent pool', async () => { + // Setup mock axios with error response + axios.get.mockRejectedValueOnce({ + response: { + status: 404, + data: { + error: 'Pool not found', + code: 404, + }, + }, + }); + + // Make the request and expect it to be rejected + await expect( + axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/pool-info`, { + params: { + network: NETWORK, + poolAddress: 'UNKNOWN', + }, + }), + ).rejects.toMatchObject({ + response: { + status: 404, + data: { + error: 'Pool not found', + }, + }, + }); + }); + }); + + describe('Positions Owned Endpoint', () => { + test('returns and validates positions owned', async () => { + const mockResponse = loadMockResponse('positionsOwned-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('positionsOwned-CLMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/positions-owned`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data[0].baseTokenAmount).toBeGreaterThan(0); + }); + }); + + describe('Quote Swap Endpoint', () => { + test('returns and validates swap quote for BUY', async () => { + // Load mock response + const mockResponse = loadMockResponse('quoteSwap-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/quote-swap`, { + params: { + network: NETWORK, + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'BUY', + amount: 0.001, + }, + }); + + // Validate the response + expect(response.status).toBe(200); + + // Check expected mock values + expect(response.data.poolAddress).toBe(TEST_POOL_ID); // Osmo uses poolId for swaps + expect(response.data.amountIn).toBeGreaterThan(1); + expect(response.data.amountOut).toBeGreaterThan(0); + + // Verify axios was called with correct parameters + expect(axios.get).toHaveBeenCalledWith( + `http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/quote-swap`, + expect.objectContaining({ + params: expect.objectContaining({ + network: NETWORK, + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'BUY', + amount: 0.001, + }), + }), + ); + }); + }); + + describe('Execute Swap Endpoint', () => { + test('returns successful swap execution', async () => { + // Mock a quote-swap response to use as input for execute-swap + const executeSwapRequest = loadMockResponse('executeSwap-CLMM-in'); + const executeSwapResponse = loadMockResponse('executeSwap-CLMM-out'); + + // Setup mock axios for the execute-swap request + axios.post.mockResolvedValueOnce({ + status: 200, + data: executeSwapResponse, + }); + + // Make the request + const response = await axios.post(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/execute-swap`, { + network: NETWORK, + baseToken: executeSwapRequest['baseToken'], + quoteToken: executeSwapRequest['quoteToken'], + side: executeSwapRequest['side'], + amount: executeSwapRequest['amount'], + wallet: executeSwapRequest['walletAddress'], + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.signature).toBeDefined(); + expect(response.data.amountIn).toBe(executeSwapResponse['amountIn']); + }); + + test('handles transaction simulation error', async () => { + // Setup mock axios with error response + axios.post.mockRejectedValueOnce({ + response: { + status: 500, + data: { + error: 'InternalServerError', + message: 'Transaction simulation failed', + code: 500, + }, + }, + }); + + // Make the request and expect it to be rejected + await expect( + axios.post(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/execute-swap`, { + network: NETWORK, + baseToken: BASE_TOKEN, + quoteToken: QUOTE_TOKEN, + side: 'SELL', + amount: 1000000.0, + wallet: TEST_WALLET, + }), + ).rejects.toMatchObject({ + response: { + status: 500, + data: { + error: 'InternalServerError', + }, + }, + }); + }); + }); + + describe('Position Info Endpoint', () => { + test('returns and validates position info', async () => { + const mockResponse = loadMockResponse('positionInfo-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('positionInfo-CLMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/position-info`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.poolAddress).toBe(TEST_POOL_ADDRESS); + expect(response.data.baseTokenAmount).toBeGreaterThan(0); + }); + }); + + describe('Add Liquidity Endpoint', () => { + test('returns successful liquidity addition', async () => { + const mockResponse = loadMockResponse('addLiquidity-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('addLiquidity-CLMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/add-liquidity`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.data.quoteTokenAmountAdded).toBeGreaterThan(0); + }); + }); + + describe('Open Position Endpoint', () => { + test('returns successful open position', async () => { + const mockResponse = loadMockResponse('openPosition-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('openPosition-CLMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/open-position`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.data.baseTokenAmountAdded).toBeGreaterThan(0); + }); + }); + + describe('Close Position Endpoint', () => { + test('returns successful close position', async () => { + const mockResponse = loadMockResponse('closePosition-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('closePosition-CLMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/close-position`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.data.baseTokenAmountRemoved).toBeGreaterThan(0); + }); + }); + + describe('Collect Fees Endpoint', () => { + test('returns successful collect fees', async () => { + const mockResponse = loadMockResponse('collectFees-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('collectFees-CLMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/collect-fees`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.data.quoteFeeAmountCollected * -1).toBeGreaterThan(0); + }); + }); + + describe('Remove Liquidity Endpoint', () => { + test('returns successful liquidity remove', async () => { + const mockResponse = loadMockResponse('removeLiquidity-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('removeLiquidity-CLMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/remove-liquidity`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.data.baseTokenAmountRemoved).toBeGreaterThan(0); + }); + }); + + describe('fetch pools Endpoint', () => { + test('fetch pools', async () => { + const mockResponse = loadMockResponse('fetchPools-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('fetchPools-CLMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/fetch-pools`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.length).toBeGreaterThan(0); + }); + }); + + describe('pool info Endpoint', () => { + test('pool info', async () => { + const mockResponse = loadMockResponse('poolInfo-CLMM-out'); + + // Setup mock axios + axios.get.mockResolvedValueOnce({ + status: 200, + data: mockResponse, + }); + + const mockRequest = loadMockResponse('poolInfo-CLMM-in'); + // Make the request + const response = await axios.get(`http://localhost:15888/connectors/${CONNECTOR}/${PROTOCOL}/pool-info`, { + params: mockRequest, + }); + + // Validate the response + expect(response.status).toBe(200); + expect(response.data.baseTokenAmount).toBeGreaterThan(0); + }); + }); +}); diff --git a/test/connectors/osmosis/mocks/addLiquidity-CLMM-in.json b/test/connectors/osmosis/mocks/addLiquidity-CLMM-in.json new file mode 100644 index 0000000000..a83613aebf --- /dev/null +++ b/test/connectors/osmosis/mocks/addLiquidity-CLMM-in.json @@ -0,0 +1,8 @@ +{ + "positionAddress": "3492", + "baseTokenAmount": 0.0002, + "quoteTokenAmount": 0.1, + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "slippagePct": 100 +} diff --git a/test/connectors/osmosis/mocks/addLiquidity-CLMM-out.json b/test/connectors/osmosis/mocks/addLiquidity-CLMM-out.json new file mode 100644 index 0000000000..9773d9ee47 --- /dev/null +++ b/test/connectors/osmosis/mocks/addLiquidity-CLMM-out.json @@ -0,0 +1,10 @@ +{ + "data": { + "fee": null, + "baseTokenAmountAdded": 0, + "quoteTokenAmountAdded": 0.135913, + "newPositionAddress": "3493" + }, + "signature": "C1653509563DD5BA8F99AE96ADCA99442C704F747295CF62147B15B20FFE2FEE", + "status": 0 +} diff --git a/test/connectors/osmosis/mocks/addLiquidity-GAMM-in.json b/test/connectors/osmosis/mocks/addLiquidity-GAMM-in.json new file mode 100644 index 0000000000..c77b47c43b --- /dev/null +++ b/test/connectors/osmosis/mocks/addLiquidity-GAMM-in.json @@ -0,0 +1,8 @@ +{ + "poolAddress": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t", + "baseTokenAmount": 0.01, + "quoteTokenAmount": 0, + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "slippagePct": 100 +} diff --git a/test/connectors/osmosis/mocks/addLiquidity-GAMM-out.json b/test/connectors/osmosis/mocks/addLiquidity-GAMM-out.json new file mode 100644 index 0000000000..98eac9bf71 --- /dev/null +++ b/test/connectors/osmosis/mocks/addLiquidity-GAMM-out.json @@ -0,0 +1,9 @@ +{ + "data": { + "fee": 0, + "baseTokenAmountAdded": 0.01, + "quoteTokenAmountAdded": 0.006707 + }, + "signature": "9AA300C5E03350BB239B605A3A7BEDB41F34D61B6839C6393E4CCB0E591A4D3F", + "status": 0 +} diff --git a/test/connectors/osmosis/mocks/addWallet-in.json b/test/connectors/osmosis/mocks/addWallet-in.json new file mode 100644 index 0000000000..54c61a883c --- /dev/null +++ b/test/connectors/osmosis/mocks/addWallet-in.json @@ -0,0 +1,4 @@ +{ + "privateKey": "2e8be986f72f76dba7f8448b2e2342d3297cd628cf08aad9b90098102824f9d5", + "chain": "cosmos" +} diff --git a/test/connectors/osmosis/mocks/addWallet-out.json b/test/connectors/osmosis/mocks/addWallet-out.json new file mode 100644 index 0000000000..bca22b70b8 --- /dev/null +++ b/test/connectors/osmosis/mocks/addWallet-out.json @@ -0,0 +1 @@ +[["osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs"]] diff --git a/test/connectors/osmosis/mocks/balances-ALL-out.json b/test/connectors/osmosis/mocks/balances-ALL-out.json new file mode 100644 index 0000000000..bf197483c0 --- /dev/null +++ b/test/connectors/osmosis/mocks/balances-ALL-out.json @@ -0,0 +1,10 @@ +{ + "network": "testnet", + "balances": { + "gamm/pool/62": 36022031254.00335, + "ION": 0.093516, + "OSMO": 151.244891, + "ibc/A8C2D23A1E6F95DA4E48BA349667E322BD7A6C996D8A4AAE8BA72E190F3D1477": 0.040924 + }, + "wallet": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" +} diff --git a/test/connectors/osmosis/mocks/balances-OSMO-out.json b/test/connectors/osmosis/mocks/balances-OSMO-out.json new file mode 100644 index 0000000000..bf197483c0 --- /dev/null +++ b/test/connectors/osmosis/mocks/balances-OSMO-out.json @@ -0,0 +1,10 @@ +{ + "network": "testnet", + "balances": { + "gamm/pool/62": 36022031254.00335, + "ION": 0.093516, + "OSMO": 151.244891, + "ibc/A8C2D23A1E6F95DA4E48BA349667E322BD7A6C996D8A4AAE8BA72E190F3D1477": 0.040924 + }, + "wallet": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" +} diff --git a/test/connectors/osmosis/mocks/block-out.json b/test/connectors/osmosis/mocks/block-out.json new file mode 100644 index 0000000000..58bbbf615c --- /dev/null +++ b/test/connectors/osmosis/mocks/block-out.json @@ -0,0 +1 @@ +41884325 diff --git a/test/connectors/osmosis/mocks/closePosition-CLMM-in.json b/test/connectors/osmosis/mocks/closePosition-CLMM-in.json new file mode 100644 index 0000000000..8b66e88d9e --- /dev/null +++ b/test/connectors/osmosis/mocks/closePosition-CLMM-in.json @@ -0,0 +1,5 @@ +{ + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "positionAddress": "3493" +} diff --git a/test/connectors/osmosis/mocks/closePosition-CLMM-out.json b/test/connectors/osmosis/mocks/closePosition-CLMM-out.json new file mode 100644 index 0000000000..a1867cae6c --- /dev/null +++ b/test/connectors/osmosis/mocks/closePosition-CLMM-out.json @@ -0,0 +1,12 @@ +{ + "signature": "4C1C3D9EEFA83EB9059A52C57280E02F2E60E1D2384E46A937E192D71E29F3BF", + "status": 0, + "data": { + "fee": 0, + "baseTokenAmountRemoved": 0.023782, + "quoteTokenAmountRemoved": 0, + "baseFeeAmountCollected": -0.013933, + "quoteFeeAmountCollected": 0, + "positionRentRefunded": 0 + } +} diff --git a/test/connectors/osmosis/mocks/collectFees-CLMM-in.json b/test/connectors/osmosis/mocks/collectFees-CLMM-in.json new file mode 100644 index 0000000000..f74a247919 --- /dev/null +++ b/test/connectors/osmosis/mocks/collectFees-CLMM-in.json @@ -0,0 +1,5 @@ +{ + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "positionAddress": "2399" +} diff --git a/test/connectors/osmosis/mocks/collectFees-CLMM-out.json b/test/connectors/osmosis/mocks/collectFees-CLMM-out.json new file mode 100644 index 0000000000..4716a7639a --- /dev/null +++ b/test/connectors/osmosis/mocks/collectFees-CLMM-out.json @@ -0,0 +1,9 @@ +{ + "signature": "24D3972857A4CF181D2BF96E18DADE74FD1A205B7E4994F0AE21BAC81C351425", + "status": 0, + "data": { + "fee": 0, + "baseFeeAmountCollected": 0, + "quoteFeeAmountCollected": -0.013736 + } +} diff --git a/test/connectors/osmosis/mocks/estimateGas-out.json b/test/connectors/osmosis/mocks/estimateGas-out.json new file mode 100644 index 0000000000..e03cea95c6 --- /dev/null +++ b/test/connectors/osmosis/mocks/estimateGas-out.json @@ -0,0 +1,8 @@ +{ + "timestamp": 1764052784917, + "denomination": "OSMO", + "feePerComputeUnit": 2000000, + "computeUnits": 0, + "feeAsset": "OSMO", + "fee": 0.03 +} diff --git a/test/connectors/osmosis/mocks/executeSwap-CLMM-in.json b/test/connectors/osmosis/mocks/executeSwap-CLMM-in.json new file mode 100644 index 0000000000..7f8d21baf7 --- /dev/null +++ b/test/connectors/osmosis/mocks/executeSwap-CLMM-in.json @@ -0,0 +1,10 @@ +{ + "baseToken": "ION", + "quoteToken": "OSMO", + "amount": 0.0001, + "side": "BUY", + "slippagePct": 99, + "chains": "cosmos", + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" +} diff --git a/test/connectors/osmosis/mocks/executeSwap-CLMM-out.json b/test/connectors/osmosis/mocks/executeSwap-CLMM-out.json new file mode 100644 index 0000000000..2e36e15721 --- /dev/null +++ b/test/connectors/osmosis/mocks/executeSwap-CLMM-out.json @@ -0,0 +1,13 @@ +{ + "data": { + "tokenIn": "ION", + "tokenOut": "OSMO", + "amountIn": 0.059125, + "amountOut": 100, + "fee": 9213, + "baseTokenBalanceChange": -100, + "quoteTokenBalanceChange": 49912 + }, + "signature": "C865071240D97311DA561327BA15EE26694FC445F5C0F43832954E6F08A8C171", + "status": 0 +} diff --git a/test/connectors/osmosis/mocks/executeSwap-GAMM-in.json b/test/connectors/osmosis/mocks/executeSwap-GAMM-in.json new file mode 100644 index 0000000000..7f8d21baf7 --- /dev/null +++ b/test/connectors/osmosis/mocks/executeSwap-GAMM-in.json @@ -0,0 +1,10 @@ +{ + "baseToken": "ION", + "quoteToken": "OSMO", + "amount": 0.0001, + "side": "BUY", + "slippagePct": 99, + "chains": "cosmos", + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" +} diff --git a/test/connectors/osmosis/mocks/executeSwap-GAMM-out.json b/test/connectors/osmosis/mocks/executeSwap-GAMM-out.json new file mode 100644 index 0000000000..8e3af2434a --- /dev/null +++ b/test/connectors/osmosis/mocks/executeSwap-GAMM-out.json @@ -0,0 +1,13 @@ +{ + "data": { + "tokenIn": "ION", + "tokenOut": "OSMO", + "amountIn": 0.03094, + "amountOut": 100, + "fee": 8546, + "baseTokenBalanceChange": -100, + "quoteTokenBalanceChange": 22394 + }, + "signature": "80E911D530A358CCC18D6A7E47EDF296E7FF280D41FF65351B57D5BB89038610", + "status": 0 +} diff --git a/test/connectors/osmosis/mocks/executeSwap-GAMM-reverse-in.json b/test/connectors/osmosis/mocks/executeSwap-GAMM-reverse-in.json new file mode 100644 index 0000000000..8fa62d83bd --- /dev/null +++ b/test/connectors/osmosis/mocks/executeSwap-GAMM-reverse-in.json @@ -0,0 +1,10 @@ +{ + "baseToken": "OSMO", + "quoteToken": "ION", + "amount": 0.1, + "side": "BUY", + "slippagePct": 99, + "chains": "cosmos", + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" +} diff --git a/test/connectors/osmosis/mocks/executeSwap-GAMM-reverse-out.json b/test/connectors/osmosis/mocks/executeSwap-GAMM-reverse-out.json new file mode 100644 index 0000000000..fe0cc709eb --- /dev/null +++ b/test/connectors/osmosis/mocks/executeSwap-GAMM-reverse-out.json @@ -0,0 +1,13 @@ +{ + "data": { + "tokenIn": "OSMO", + "tokenOut": "ION", + "amountIn": 0.000316, + "amountOut": 100000, + "fee": 8310, + "baseTokenBalanceChange": -108310, + "quoteTokenBalanceChange": 316 + }, + "signature": "A02A2DBAE5251BAD265746DE548CB3530F88F87AAE26DFBDC209E09BF82FF3A5", + "status": 0 +} diff --git a/test/connectors/osmosis/mocks/fetchPools-CLMM-in.json b/test/connectors/osmosis/mocks/fetchPools-CLMM-in.json new file mode 100644 index 0000000000..2dc51a3a6f --- /dev/null +++ b/test/connectors/osmosis/mocks/fetchPools-CLMM-in.json @@ -0,0 +1,5 @@ +{ + "network": "testnet", + "tokenA": "ION", + "tokenB": "OSMO" +} diff --git a/test/connectors/osmosis/mocks/fetchPools-CLMM-out.json b/test/connectors/osmosis/mocks/fetchPools-CLMM-out.json new file mode 100644 index 0000000000..185925d959 --- /dev/null +++ b/test/connectors/osmosis/mocks/fetchPools-CLMM-out.json @@ -0,0 +1,200 @@ +[ + { + "$typeUrl": "/osmosis.concentratedliquidity.v1beta1.Pool", + "address": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6", + "incentivesAddress": "osmo1jq70f9w8ggln5608qplu5y85497577dgtyaneay0zw0rptjnlynsj7aycp", + "spreadRewardsAddress": "osmo1y5uahhsagtzldjd704ut0xtpkaz6ruwl4gmuas38fwwutx372xfqegfpy7", + "id": "1269", + "token0": "uosmo", + "token1": "uion", + "currentTick": "-26333400", + "tickSpacing": "100", + "exponentAtPriceOne": "-6", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "currentTickLiquidity": "1603120.617780494419507982", + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.concentratedliquidity.v1beta1.Pool", + "address": "osmo12vw7dlafwxefct6ntqr6hmn6x5h0u4yrll54u4x38fad838ya6gs0w57k0", + "incentivesAddress": "osmo1vvlfgza07am2494cqfgca0fqcg8h4f6a6qj5cy05ynd9rh0kpx8qyjext2", + "spreadRewardsAddress": "osmo160qdm572d08zu87mjeh4y3g860swsmuln09vw4p2t2y8kphqd0yshrujgw", + "id": "1278", + "token0": "uosmo", + "token1": "uion", + "currentTick": "-27000000", + "tickSpacing": "100", + "exponentAtPriceOne": "-6", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "currentTickLiquidity": "31622.7766016837940001", + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.concentratedliquidity.v1beta1.Pool", + "address": "osmo12lmlcm2phju6ek4fexvr5pgkfr0ksdc49qytxyu6fqkvufxdt6ushqdmay", + "incentivesAddress": "osmo1nzeqrfg2yua2tsqk7qaw8hdff35f4mjtag06nx95y7d4ve4kkw8sk93d80", + "spreadRewardsAddress": "osmo1zwfeqkgertrarzwqtf2alakt5y9n3rtt535zply5u0x4at23d0dqxvz025", + "id": "1280", + "token0": "uion", + "token1": "uosmo", + "currentTick": "11333300", + "tickSpacing": "100", + "exponentAtPriceOne": "-6", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "currentTickLiquidity": "170262.794416288251794274", + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.concentratedliquidity.v1beta1.Pool", + "address": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "incentivesAddress": "osmo1clr4uj97mfymv23hzds5j2zypkdwcert0386kjuscmpg0vh222xqwal289", + "spreadRewardsAddress": "osmo19thtcxk4gcv6xyn4yhqhuhkr8mp4ss76vz4uq2tq9ddzcxvkw5aqqv7eg0", + "id": "130", + "token0": "uion", + "token1": "uosmo", + "currentTick": "20121165", + "tickSpacing": "100", + "exponentAtPriceOne": "-6", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "currentTickLiquidity": "558616.499028197018370007", + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.concentratedliquidity.v1beta1.Pool", + "address": "osmo16j5js5txt833hmacw5txylxexgqa22nqj6405qhg7v0tlc3gjupsf6ltrh", + "incentivesAddress": "osmo1va24eddq8mg07q08mg5693cwa5uzpx9ctt76ut0zv4x5drvwc7js7u9ura", + "spreadRewardsAddress": "osmo1w5swyez4hq6wjvgglpr3lgyztxkn8flrjdfzsavvxrdn5hj0sezqrl0hz8", + "id": "60", + "token0": "uion", + "token1": "uosmo", + "currentTick": "18408085", + "tickSpacing": "100", + "exponentAtPriceOne": "-6", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "currentTickLiquidity": "14064015.633485401746996635", + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.concentratedliquidity.v1beta1.Pool", + "address": "osmo16gvagdqh9pz8cyrtzg2sncq3zyha3ghfjq3zp68xlcghn7y6jjws4dy5ea", + "incentivesAddress": "osmo145mylmeufacfq45q26yqylsccsxlct3s0cafzyp5sucytxa6hppq3zfrf4", + "spreadRewardsAddress": "osmo1yxgtlra54gq0agvwuajzqkv5mllhyzu8wcasncs4qhmsfwyg792q4rmq0q", + "id": "63", + "token0": "uion", + "token1": "uosmo", + "currentTick": "20092670", + "tickSpacing": "100", + "exponentAtPriceOne": "-6", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "currentTickLiquidity": "35170710784.452806907829766201", + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.concentratedliquidity.v1beta1.Pool", + "address": "osmo13ggnfrm4skd5ku4xyfrzymr4lfw9p4354tnpkut8sztaljv9gmtq9kchrk", + "incentivesAddress": "osmo13teyr547yypn4wv0u0jwmqukjt0j87jyjsnvj0qn23zjc97fn42qm5l6mw", + "spreadRewardsAddress": "osmo1wzv9vxxxwze9hsyntjyst9dhn5jdu98qscn7qdu5vhn9gnvl5phqz77ex6", + "id": "74", + "token0": "uion", + "token1": "uosmo", + "currentTick": "20106770", + "tickSpacing": "100", + "exponentAtPriceOne": "-6", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "currentTickLiquidity": "939343805.711495145365943259", + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.concentratedliquidity.v1beta1.Pool", + "address": "osmo1k7gek8m33r7fwzu7fpemc3qpecs670qz26x524zu7230klxt8rrq5nyxtv", + "incentivesAddress": "osmo1kpkufphwc9yxv9lta0zaj6v7wvh54mzt2e7gqa5pmvg9naucsa3s2ahf2z", + "spreadRewardsAddress": "osmo1s0lq8qcntm463latvppddy69yxv6fvyv28yg22n0hrk9we60kveswfk4x6", + "id": "79", + "token0": "uion", + "token1": "uosmo", + "currentTick": "20233385", + "tickSpacing": "100", + "exponentAtPriceOne": "-6", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "currentTickLiquidity": "133804.140018392937099336", + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.concentratedliquidity.v1beta1.Pool", + "address": "osmo1vm2fn0p9zs9ykjktxjkq9hawgyx0m8ye0a6qspew576vdu02acas9w9zl6", + "incentivesAddress": "osmo1m7ey2vfcrqz306z308nnd2qtaltvmmmm4xejrynch4cvky43k3csme6muw", + "spreadRewardsAddress": "osmo12q4mt0yljkgsdk0lnr2t2w4k9sp0u9k6zveh7vu7d7rl7vp6ym6qru53z5", + "id": "999", + "token0": "uosmo", + "token1": "uion", + "currentTick": "-28526602", + "tickSpacing": "100", + "exponentAtPriceOne": "-6", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "currentTickLiquidity": "114294.310830694473508256", + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "", + "swapFee": "0", + "exitFee": "0" + } +] diff --git a/test/connectors/osmosis/mocks/fetchPools-GAMM-in.json b/test/connectors/osmosis/mocks/fetchPools-GAMM-in.json new file mode 100644 index 0000000000..2dc51a3a6f --- /dev/null +++ b/test/connectors/osmosis/mocks/fetchPools-GAMM-in.json @@ -0,0 +1,5 @@ +{ + "network": "testnet", + "tokenA": "ION", + "tokenB": "OSMO" +} diff --git a/test/connectors/osmosis/mocks/fetchPools-GAMM-out.json b/test/connectors/osmosis/mocks/fetchPools-GAMM-out.json new file mode 100644 index 0000000000..baa011fc26 --- /dev/null +++ b/test/connectors/osmosis/mocks/fetchPools-GAMM-out.json @@ -0,0 +1,1027 @@ +[ + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1", + "futurePoolGovernor": "", + "totalShares": { + "denom": "gamm/pool/1", + "amount": "15358661852992592583562" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "2507999771" + }, + "weight": "1073741824" + }, + { + "token": { + "denom": "uosmo", + "amount": "3151008345006" + }, + "weight": "4294967296" + } + ], + "totalWeight": "5368709120", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1", + "swapFee": "0.005", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo17svzplxq3dmkz0atv6vtepftvtfl5daxuajtzxjwchnyjumupg5q649708", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "62", + "futurePoolGovernor": "24h", + "totalShares": { + "denom": "gamm/pool/62", + "amount": "53632064162512560180" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "521279" + }, + "weight": "536870912000000" + }, + { + "token": { + "denom": "uosmo", + "amount": "161814250" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "1073741824000000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/62", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo17yhgagvkwhzkd2yt5dmh5n0qfyzlp53dpuldtk2eg3egum8njmqqpaj3am", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "934", + "futurePoolGovernor": "24h", + "totalShares": { + "denom": "gamm/pool/934", + "amount": "50000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "7520" + }, + "weight": "536870912000000" + }, + { + "token": { + "denom": "uosmo", + "amount": "3324469" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "1073741824000000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/934", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo18ue58qakuanxkhj2dxx3mcw7244rvlgkuc79p738nra7skpj67vqt67qwq", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "935", + "futurePoolGovernor": "osmo1wrkp789lzu53w8g20avhzjnm7d8xmmuqmx53ytcqgwng0mh00a7sffsjhd", + "totalShares": { + "denom": "gamm/pool/935", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1000" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "10000000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "590558003200000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/935", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1yn9pgwqlt32y8d99vk3vxq7tcjaml3jlwvjjwk66tx30tyt2pcusurnrcm", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "936", + "futurePoolGovernor": "osmo1wrkp789lzu53w8g20avhzjnm7d8xmmuqmx53ytcqgwng0mh00a7sffsjhd", + "totalShares": { + "denom": "gamm/pool/936", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1000" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "10000000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "590558003200000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/936", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo13au2kel92hpc5yucp9vdan2qpc3ngcrc8njp77eaq2t9hjxelmusd3nzyk", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "937", + "futurePoolGovernor": "osmo1r0r9xkus9e9c804yhpc5a3zunlupvsmmx47wmxrndnuj8htr0tlqzg7qns", + "totalShares": { + "denom": "gamm/pool/937", + "amount": "150000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1500" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "15000000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "590558003200000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/937", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1k945cjqpl5a2gnks88f5c4vup8zzte0lyaxxgcr79v09hn5vw62se77f4l", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "938", + "futurePoolGovernor": "osmo1r0r9xkus9e9c804yhpc5a3zunlupvsmmx47wmxrndnuj8htr0tlqzg7qns", + "totalShares": { + "denom": "gamm/pool/938", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1000" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "10000000" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/938", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1yjzkt8p665wagcvvrdl9yqc5xg9kpa8z6c5xj5uydwr9ncjnz2tsx793kw", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "939", + "futurePoolGovernor": "osmo12l60kgf8rqxwhaqarnrwczsvtm48fale3kj8l85xt7w6dazxaaaqm8jx89", + "totalShares": { + "denom": "gamm/pool/939", + "amount": "457069800000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "25221" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "8284074" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/939", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1w433z7r0l5pgzmjtlnqvhkmdd2saz6edhxm2sw9xwpxd5c9y4cpq9nq9ct", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "940", + "futurePoolGovernor": "osmo1c2pgg87er3lg5wwrg8n475rdgvgjpqrz2mv3t7dzvl8egjpq95xsjquzc6", + "totalShares": { + "denom": "gamm/pool/940", + "amount": "50000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "500" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "5000000" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/940", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1gxx4lx3qfghak8hx9eknn5fqq3uz5g9hccv44ktaczj2ka53sjpqg6hpel", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "943", + "futurePoolGovernor": "24h", + "totalShares": { + "denom": "gamm/pool/943", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "3199" + }, + "weight": "536870912000000" + }, + { + "token": { + "denom": "uosmo", + "amount": "500000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "1073741824000000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/943", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1x6m3jf33gl2gc92w9v5x7rhh4jnjauq3897nk406rwxw4nv7zw7qmpwrqu", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "952", + "futurePoolGovernor": "24h", + "totalShares": { + "denom": "gamm/pool/952", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "3199" + }, + "weight": "536870912000000" + }, + { + "token": { + "denom": "uosmo", + "amount": "500000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "1073741824000000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/952", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1d2xfry4akm8fkyf6mvl7c272udvpxp7lqqtpe5a27ckyrwvq8mssryay6g", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "963", + "futurePoolGovernor": "24h", + "totalShares": { + "denom": "gamm/pool/963", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "2712" + }, + "weight": "536870912000000" + }, + { + "token": { + "denom": "uosmo", + "amount": "590000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "1073741824000000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/963", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo15yg0dqg5awfxrm32t36a2eeywx5kfnkw5g25juz0tfdhl2932q6qyq4l2z", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "984", + "futurePoolGovernor": "24h", + "totalShares": { + "denom": "gamm/pool/984", + "amount": "1100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "22441" + }, + "weight": "536870912000000" + }, + { + "token": { + "denom": "uosmo", + "amount": "6900000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "1073741824000000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/984", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo19f2yx94azpq7d0e5fc3hpr743c60r02paqc7l9w4yzurz7sufy7q3rssec", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "987", + "futurePoolGovernor": "24h", + "totalShares": { + "denom": "gamm/pool/987", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "2580" + }, + "weight": "536870912000000" + }, + { + "token": { + "denom": "uosmo", + "amount": "620000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "1073741824000000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/987", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1jmzcjzx5n3kjputv3n03p2ta2k7fdaqqxn8hqc8y4lfqtspg3hzq5uff8n", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1016", + "futurePoolGovernor": "24h", + "totalShares": { + "denom": "gamm/pool/1016", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "3200" + }, + "weight": "536870912000000" + }, + { + "token": { + "denom": "uosmo", + "amount": "500000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "1073741824000000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1016", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1rh4jr7ljfvdu2tptk6vvp0qja99p750y9fj0xfdcdhtua8jnjk5sultrnt", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1029", + "futurePoolGovernor": "osmo1c2pgg87er3lg5wwrg8n475rdgvgjpqrz2mv3t7dzvl8egjpq95xsjquzc6", + "totalShares": { + "denom": "gamm/pool/1029", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1000" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "10000000" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1029", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1hf3ugena0jv5sfsehj0vd8nts57qu7kwhdytvraqepenr350japsvwp95q", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1155", + "futurePoolGovernor": "osmo1r2vw2g92f5mt78mj029qlllfsfhrgyh6pzc4zgacllwg7p6x40rqnxgndc", + "totalShares": { + "denom": "gamm/pool/1155", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1015" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "49261084" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1155", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1nprvwcn7npspcvpx3y890wyc6lkg3pa5zmfmaszh5zk5x4faxhtsp349ls", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1156", + "futurePoolGovernor": "osmo128tuz2xrm69r530z37h052ve926zt0ky8w43s92wxn7kwv7p9p3smp3lmt", + "totalShares": { + "denom": "gamm/pool/1156", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1015" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "49261084" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1156", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1daj4ztx6s9rr3s32y4sqm67wq7p4c8653u0zrmfkzfqhl5d5m30qufad2z", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1157", + "futurePoolGovernor": "osmo13p3jpl7qcrv5uexytv6387pvp56xjwj7z7k2jyypqqwa28ullm0s9rlt7w", + "totalShares": { + "denom": "gamm/pool/1157", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1000" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "10000000" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1157", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1dsnwhxenw422tk9t20x4mjv5vyrmmd90kky403vt9d8qym7jc8ds4lkx0x", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1158", + "futurePoolGovernor": "osmo17fc0u7jv4kcaygyn5uam838usedf9vyr06p70kah84qz4defuq9ql8dtn8", + "totalShares": { + "denom": "gamm/pool/1158", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1000" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "10000" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1158", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1gj26haupvyh6qun4fmc5xnr0tsrg9fwfvycs567u6d5qmezue22sza64gx", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1251", + "futurePoolGovernor": "osmo185w6cr5pdwt34l6d6njc2946maw62r9hlyaj0cy45rm0m0aq73wq6k78fm", + "totalShares": { + "denom": "gamm/pool/1251", + "amount": "49853055290583468696783" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "190810" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "13025315" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1251", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1p83q2g9t20lfk07w53vvy5flrvr6c6gh65xufy47546z555af88qwlwqvg", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1252", + "futurePoolGovernor": "osmo15u706f9f2mh54w986rvxcmeq86e7sy5nuvfwnjzhrjx6t9j3felsnq08ug", + "totalShares": { + "denom": "gamm/pool/1252", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1000" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "10000" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1252", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1g854f98z6c7eqwnpm7nn82d6mmunefzvy84hlgwnv58pa6wshhlsra06qu", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1253", + "futurePoolGovernor": "osmo1x2z28sqg5g7s3yyay8meht8k9xu9k4mrh8gkcj86546xszgxh9xs0urjaa", + "totalShares": { + "denom": "gamm/pool/1253", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "1000" + }, + "weight": "53687091200000" + }, + { + "token": { + "denom": "uosmo", + "amount": "10000" + }, + "weight": "53687091200000" + } + ], + "totalWeight": "107374182400000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1253", + "swapFee": "0.000000000000000001", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1xk3ms4pa63dl8tldtfpr5yes2g5a05j07s5cpgn7ucgkg2m43jsq3wmpaj", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1260", + "futurePoolGovernor": "24h", + "totalShares": { + "denom": "gamm/pool/1260", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "32136" + }, + "weight": "536870912000000" + }, + { + "token": { + "denom": "uosmo", + "amount": "10000000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "1073741824000000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1260", + "swapFee": "0", + "exitFee": "0" + }, + { + "$typeUrl": "/osmosis.gamm.v1beta1.Pool", + "address": "osmo1c4rjlrvm2u3epazr80c9reepvv9xzdh05cx5greyv899rjzgvqpsryzq4g", + "incentivesAddress": "", + "spreadRewardsAddress": "", + "id": "1270", + "futurePoolGovernor": "24h", + "totalShares": { + "denom": "gamm/pool/1270", + "amount": "100000000000000000000" + }, + "poolAssets": [ + { + "token": { + "denom": "uion", + "amount": "30000" + }, + "weight": "536870912000000" + }, + { + "token": { + "denom": "uosmo", + "amount": "10000000" + }, + "weight": "536870912000000" + } + ], + "totalWeight": "1073741824000000", + "currentTick": "0", + "tickSpacing": "0", + "exponentAtPriceOne": "0", + "fees_volume24H": 0, + "fees_spent_7d": 0, + "fees_volume7d": 0, + "myLiquidityShares": 0, + "myLiquidityDollarValue": "0", + "my_bonded_shares": "0", + "denom": "gamm/pool/1270", + "swapFee": "0.005", + "exitFee": "0" + } +] diff --git a/test/connectors/osmosis/mocks/get-token-OSMO-out.json b/test/connectors/osmosis/mocks/get-token-OSMO-out.json new file mode 100644 index 0000000000..bc689571d3 --- /dev/null +++ b/test/connectors/osmosis/mocks/get-token-OSMO-out.json @@ -0,0 +1,11 @@ +{ + "tokens": [ + { + "chainId": 0, + "address": "uosmo", + "name": "Osmosis Testnet", + "symbol": "OSMO", + "decimals": 6 + } + ] +} diff --git a/test/connectors/osmosis/mocks/openPosition-CLMM-in.json b/test/connectors/osmosis/mocks/openPosition-CLMM-in.json new file mode 100644 index 0000000000..f5530ffa40 --- /dev/null +++ b/test/connectors/osmosis/mocks/openPosition-CLMM-in.json @@ -0,0 +1,10 @@ +{ + "lowerPrice": 200, + "upperPrice": 1000, + "poolAddress": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6", + "baseTokenAmount": 0.0002, + "quoteTokenAmount": 0.1, + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "slippagePct": 100 +} diff --git a/test/connectors/osmosis/mocks/openPosition-CLMM-out.json b/test/connectors/osmosis/mocks/openPosition-CLMM-out.json new file mode 100644 index 0000000000..b27b3f254f --- /dev/null +++ b/test/connectors/osmosis/mocks/openPosition-CLMM-out.json @@ -0,0 +1,11 @@ +{ + "signature": "6CBB287452B2C0F8D02AE233186653E02BCB95AB46A78B3DDE92B59259FB536E", + "status": 0, + "data": { + "fee": 0, + "baseTokenAmountAdded": 0.014771, + "quoteTokenAmountAdded": 0, + "positionAddress": "3492", + "positionRent": 0 + } +} diff --git a/test/connectors/osmosis/mocks/poll-in.json b/test/connectors/osmosis/mocks/poll-in.json new file mode 100644 index 0000000000..e520084b50 --- /dev/null +++ b/test/connectors/osmosis/mocks/poll-in.json @@ -0,0 +1,6 @@ +{ + "network": "testnet", + "signature": "344A0C038C05D1FA938E78828925109879E30C397100BD84D0BA08A463B2FF82", + "tokens": [], + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" +} diff --git a/test/connectors/osmosis/mocks/poll-out.json b/test/connectors/osmosis/mocks/poll-out.json new file mode 100644 index 0000000000..b95dcf7e77 --- /dev/null +++ b/test/connectors/osmosis/mocks/poll-out.json @@ -0,0 +1,354 @@ +{ + "tokenBalanceChanges": { + "gamm/pool/1": 121988640257602, + "OSMO": -0.006701, + "ION": -0.0001 + }, + "currentBlock": 41884325, + "signature": "344A0C038C05D1FA938E78828925109879E30C397100BD84D0BA08A463B2FF82", + "txStatus": 0, + "txBlock": 41207531, + "fee": 6701, + "txData": [ + { + "type": "coin_spent", + "attributes": [ + { + "key": "spender", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "index": true + }, + { + "key": "amount", + "value": "6701uosmo", + "index": true + } + ] + }, + { + "type": "coin_received", + "attributes": [ + { + "key": "receiver", + "value": "osmo17xpfvakm2amg962yls6f84z3kell8c5lczssa0", + "index": true + }, + { + "key": "amount", + "value": "6701uosmo", + "index": true + } + ] + }, + { + "type": "transfer", + "attributes": [ + { + "key": "recipient", + "value": "osmo17xpfvakm2amg962yls6f84z3kell8c5lczssa0", + "index": true + }, + { + "key": "sender", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "index": true + }, + { + "key": "amount", + "value": "6701uosmo", + "index": true + } + ] + }, + { + "type": "message", + "attributes": [ + { + "key": "sender", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "index": true + } + ] + }, + { + "type": "tx", + "attributes": [ + { + "key": "fee", + "value": "6701uosmo", + "index": true + } + ] + }, + { + "type": "tx", + "attributes": [ + { + "key": "acc_seq", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs/1416", + "index": true + } + ] + }, + { + "type": "tx", + "attributes": [ + { + "key": "signature", + "value": "REx8LQ9xKWxInXJYwNoMZzYd5tNufJlcR8RfJqdeEnkHkWTDKdqvIWK9PwuwPCH1U0DBI6V9jPTGUN48eSmt/Q==", + "index": true + } + ] + }, + { + "type": "message", + "attributes": [ + { + "key": "action", + "value": "/osmosis.gamm.v1beta1.MsgJoinSwapExternAmountIn", + "index": true + }, + { + "key": "sender", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "coin_spent", + "attributes": [ + { + "key": "spender", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "index": true + }, + { + "key": "amount", + "value": "100uion", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "coin_received", + "attributes": [ + { + "key": "receiver", + "value": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t", + "index": true + }, + { + "key": "amount", + "value": "100uion", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "transfer", + "attributes": [ + { + "key": "recipient", + "value": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t", + "index": true + }, + { + "key": "sender", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "index": true + }, + { + "key": "amount", + "value": "100uion", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "message", + "attributes": [ + { + "key": "sender", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "coin_received", + "attributes": [ + { + "key": "receiver", + "value": "osmo1c9y7crgg6y9pfkq0y8mqzknqz84c3etr0kpcvj", + "index": true + }, + { + "key": "amount", + "value": "121988640257602gamm/pool/1", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "coinbase", + "attributes": [ + { + "key": "minter", + "value": "osmo1c9y7crgg6y9pfkq0y8mqzknqz84c3etr0kpcvj", + "index": true + }, + { + "key": "amount", + "value": "121988640257602gamm/pool/1", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "coin_spent", + "attributes": [ + { + "key": "spender", + "value": "osmo1c9y7crgg6y9pfkq0y8mqzknqz84c3etr0kpcvj", + "index": true + }, + { + "key": "amount", + "value": "121988640257602gamm/pool/1", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "coin_received", + "attributes": [ + { + "key": "receiver", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "index": true + }, + { + "key": "amount", + "value": "121988640257602gamm/pool/1", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "transfer", + "attributes": [ + { + "key": "recipient", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "index": true + }, + { + "key": "sender", + "value": "osmo1c9y7crgg6y9pfkq0y8mqzknqz84c3etr0kpcvj", + "index": true + }, + { + "key": "amount", + "value": "121988640257602gamm/pool/1", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "message", + "attributes": [ + { + "key": "sender", + "value": "osmo1c9y7crgg6y9pfkq0y8mqzknqz84c3etr0kpcvj", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + }, + { + "type": "pool_joined", + "attributes": [ + { + "key": "module", + "value": "gamm", + "index": true + }, + { + "key": "sender", + "value": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "index": true + }, + { + "key": "pool_id", + "value": "1", + "index": true + }, + { + "key": "tokens_in", + "value": "100uion", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] + } + ] +} diff --git a/test/connectors/osmosis/mocks/poolInfo-CLMM-fail-in.json b/test/connectors/osmosis/mocks/poolInfo-CLMM-fail-in.json new file mode 100644 index 0000000000..70d92f1d2c --- /dev/null +++ b/test/connectors/osmosis/mocks/poolInfo-CLMM-fail-in.json @@ -0,0 +1,4 @@ +{ + "network": "testnet", + "poolAddress": "UNKNOWN" +} diff --git a/test/connectors/osmosis/mocks/poolInfo-CLMM-fail-out.json b/test/connectors/osmosis/mocks/poolInfo-CLMM-fail-out.json new file mode 100644 index 0000000000..da3aa3696b --- /dev/null +++ b/test/connectors/osmosis/mocks/poolInfo-CLMM-fail-out.json @@ -0,0 +1,3 @@ +{ + "statusCode": 404 +} diff --git a/test/connectors/osmosis/mocks/poolInfo-CLMM-in.json b/test/connectors/osmosis/mocks/poolInfo-CLMM-in.json new file mode 100644 index 0000000000..edcbaefccd --- /dev/null +++ b/test/connectors/osmosis/mocks/poolInfo-CLMM-in.json @@ -0,0 +1,4 @@ +{ + "network": "testnet", + "poolAddress": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6" +} diff --git a/test/connectors/osmosis/mocks/poolInfo-CLMM-out.json b/test/connectors/osmosis/mocks/poolInfo-CLMM-out.json new file mode 100644 index 0000000000..3eb0f306b1 --- /dev/null +++ b/test/connectors/osmosis/mocks/poolInfo-CLMM-out.json @@ -0,0 +1,11 @@ +{ + "address": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6", + "baseTokenAddress": "uosmo", + "quoteTokenAddress": "uion", + "feePct": 0, + "price": 0.0016666, + "baseTokenAmount": 1603120.6177804945, + "quoteTokenAmount": 1603120.6177804945, + "binStep": 100, + "activeBinId": -26333400 +} diff --git a/test/connectors/osmosis/mocks/poolInfo-GAMM-fail-in.json b/test/connectors/osmosis/mocks/poolInfo-GAMM-fail-in.json new file mode 100644 index 0000000000..70d92f1d2c --- /dev/null +++ b/test/connectors/osmosis/mocks/poolInfo-GAMM-fail-in.json @@ -0,0 +1,4 @@ +{ + "network": "testnet", + "poolAddress": "UNKNOWN" +} diff --git a/test/connectors/osmosis/mocks/poolInfo-GAMM-fail-out.json b/test/connectors/osmosis/mocks/poolInfo-GAMM-fail-out.json new file mode 100644 index 0000000000..da3aa3696b --- /dev/null +++ b/test/connectors/osmosis/mocks/poolInfo-GAMM-fail-out.json @@ -0,0 +1,3 @@ +{ + "statusCode": 404 +} diff --git a/test/connectors/osmosis/mocks/poolInfo-GAMM-in.json b/test/connectors/osmosis/mocks/poolInfo-GAMM-in.json new file mode 100644 index 0000000000..2255ce469e --- /dev/null +++ b/test/connectors/osmosis/mocks/poolInfo-GAMM-in.json @@ -0,0 +1,4 @@ +{ + "network": "testnet", + "poolAddress": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t" +} diff --git a/test/connectors/osmosis/mocks/poolInfo-GAMM-out.json b/test/connectors/osmosis/mocks/poolInfo-GAMM-out.json new file mode 100644 index 0000000000..8519c4e360 --- /dev/null +++ b/test/connectors/osmosis/mocks/poolInfo-GAMM-out.json @@ -0,0 +1,9 @@ +{ + "address": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t", + "baseTokenAddress": "uion", + "quoteTokenAddress": "uosmo", + "feePct": 0.005, + "price": 0.0007959260957740554, + "baseTokenAmount": 2507975746, + "quoteTokenAmount": 3151015853502 +} diff --git a/test/connectors/osmosis/mocks/positionInfo-CLMM-in.json b/test/connectors/osmosis/mocks/positionInfo-CLMM-in.json new file mode 100644 index 0000000000..8b66e88d9e --- /dev/null +++ b/test/connectors/osmosis/mocks/positionInfo-CLMM-in.json @@ -0,0 +1,5 @@ +{ + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "positionAddress": "3493" +} diff --git a/test/connectors/osmosis/mocks/positionInfo-CLMM-out.json b/test/connectors/osmosis/mocks/positionInfo-CLMM-out.json new file mode 100644 index 0000000000..6a2dc6431d --- /dev/null +++ b/test/connectors/osmosis/mocks/positionInfo-CLMM-out.json @@ -0,0 +1,16 @@ +{ + "network": "testnet", + "address": "3493", + "poolAddress": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 50100, + "quoteTokenAmount": 0, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 19000000, + "upperBinId": 27000000, + "lowerPrice": 200, + "upperPrice": 1000, + "price": 0.0016666 +} diff --git a/test/connectors/osmosis/mocks/positionInfo-GAMM-in.json b/test/connectors/osmosis/mocks/positionInfo-GAMM-in.json new file mode 100644 index 0000000000..07e6eef3f4 --- /dev/null +++ b/test/connectors/osmosis/mocks/positionInfo-GAMM-in.json @@ -0,0 +1,5 @@ +{ + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "poolAddress": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t" +} diff --git a/test/connectors/osmosis/mocks/positionInfo-GAMM-out.json b/test/connectors/osmosis/mocks/positionInfo-GAMM-out.json new file mode 100644 index 0000000000..87d959c98f --- /dev/null +++ b/test/connectors/osmosis/mocks/positionInfo-GAMM-out.json @@ -0,0 +1,11 @@ +{ + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "poolAddress": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t", + "baseTokenAmount": 0, + "quoteTokenAmount": 0, + "baseTokenAddress": "", + "quoteTokenAddress": "", + "price": 0.0007959387905065114, + "lpTokenAmount": 12198727513903672 +} diff --git a/test/connectors/osmosis/mocks/positionsOwned-CLMM-in.json b/test/connectors/osmosis/mocks/positionsOwned-CLMM-in.json new file mode 100644 index 0000000000..f454da6712 --- /dev/null +++ b/test/connectors/osmosis/mocks/positionsOwned-CLMM-in.json @@ -0,0 +1,5 @@ +{ + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "poolType": "clmm" +} diff --git a/test/connectors/osmosis/mocks/positionsOwned-CLMM-out.json b/test/connectors/osmosis/mocks/positionsOwned-CLMM-out.json new file mode 100644 index 0000000000..236a14ab87 --- /dev/null +++ b/test/connectors/osmosis/mocks/positionsOwned-CLMM-out.json @@ -0,0 +1,603 @@ +{ + "0": { + "address": "964", + "poolAddress": "osmo1w5yw6cldlk0tgvjsm7vj22rw7sg6nwx9yd5rd6kc58lax5nvvyasr0he5h", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 1035, + "quoteTokenAmount": 451, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 4000000, + "upperBinId": 4700000, + "lowerPrice": 0.000005, + "upperPrice": 0.0000057, + "price": 0.000005051003 + }, + "1": { + "address": "966", + "poolAddress": "osmo1w5yw6cldlk0tgvjsm7vj22rw7sg6nwx9yd5rd6kc58lax5nvvyasr0he5h", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 1035, + "quoteTokenAmount": 451, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 4000000, + "upperBinId": 4700000, + "lowerPrice": 0.000005, + "upperPrice": 0.0000057, + "price": 0.000005051003 + }, + "2": { + "address": "1025", + "poolAddress": "osmo1w5yw6cldlk0tgvjsm7vj22rw7sg6nwx9yd5rd6kc58lax5nvvyasr0he5h", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 1035, + "quoteTokenAmount": 451, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 4000000, + "upperBinId": 4700000, + "lowerPrice": 0.000005, + "upperPrice": 0.0000057, + "price": 0.000005051003 + }, + "3": { + "address": "3480", + "poolAddress": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 100199, + "quoteTokenAmount": 0, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 19000000, + "upperBinId": 27000000, + "lowerPrice": 200, + "upperPrice": 1000, + "price": 0.0016666 + }, + "4": { + "address": "3485", + "poolAddress": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 200, + "quoteTokenAmount": 0, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 19000000, + "upperBinId": 27000000, + "lowerPrice": 200, + "upperPrice": 1000, + "price": 0.0016666 + }, + "5": { + "address": "3489", + "poolAddress": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 200, + "quoteTokenAmount": 0, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 19000000, + "upperBinId": 27000000, + "lowerPrice": 200, + "upperPrice": 1000, + "price": 0.0016666 + }, + "6": { + "address": "3492", + "poolAddress": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 200, + "quoteTokenAmount": 0, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 19000000, + "upperBinId": 27000000, + "lowerPrice": 200, + "upperPrice": 1000, + "price": 0.0016666 + }, + "7": { + "address": "2396", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "8": { + "address": "2397", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "9": { + "address": "2398", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "10": { + "address": "2399", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "11": { + "address": "2400", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "12": { + "address": "2401", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "13": { + "address": "2402", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "14": { + "address": "2403", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "15": { + "address": "2404", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "16": { + "address": "2405", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "17": { + "address": "2406", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "18": { + "address": "2407", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "19": { + "address": "2408", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "20": { + "address": "2409", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "21": { + "address": "2410", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "22": { + "address": "2411", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "23": { + "address": "2412", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 95, + "quoteTokenAmount": 60865, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "24": { + "address": "2433", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 160, + "quoteTokenAmount": 103181, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "25": { + "address": "2443", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 160, + "quoteTokenAmount": 103181, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "26": { + "address": "2447", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 160, + "quoteTokenAmount": 103181, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "27": { + "address": "2464", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 160, + "quoteTokenAmount": 103181, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "28": { + "address": "2545", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 160, + "quoteTokenAmount": 103181, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "29": { + "address": "2549", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 16, + "quoteTokenAmount": 9825, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "30": { + "address": "2554", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 153, + "quoteTokenAmount": 98245, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "31": { + "address": "2567", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 153, + "quoteTokenAmount": 98245, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "32": { + "address": "2673", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 153, + "quoteTokenAmount": 98245, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "33": { + "address": "2725", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 153, + "quoteTokenAmount": 98245, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "34": { + "address": "2726", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 153, + "quoteTokenAmount": 98245, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "35": { + "address": "2746", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 153, + "quoteTokenAmount": 98245, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 18000000, + "upperBinId": 22000000, + "lowerPrice": 100, + "upperPrice": 500, + "price": 312.1165 + }, + "36": { + "address": "2836", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 2036, + "quoteTokenAmount": 287203, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 19000000, + "upperBinId": 27000000, + "lowerPrice": 200, + "upperPrice": 1000, + "price": 312.1165 + }, + "37": { + "address": "2837", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 4070, + "quoteTokenAmount": 574261, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 19000000, + "upperBinId": 27000000, + "lowerPrice": 200, + "upperPrice": 1000, + "price": 312.1165 + }, + "38": { + "address": "2839", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 407, + "quoteTokenAmount": 57297, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 19000000, + "upperBinId": 27000000, + "lowerPrice": 200, + "upperPrice": 1000, + "price": 312.1165 + }, + "39": { + "address": "2851", + "poolAddress": "osmo12utnq8dkm4jgntgtah862vkv6zya73twaxmwx07aenq85nj0csjslcejsk", + "baseTokenAddress": "", + "quoteTokenAddress": "", + "baseTokenAmount": 188, + "quoteTokenAmount": 26407, + "baseFeeAmount": 0, + "quoteFeeAmount": 0, + "lowerBinId": 19000000, + "upperBinId": 27000000, + "lowerPrice": 200, + "upperPrice": 1000, + "price": 312.1165 + }, + "network": "testnet" +} diff --git a/test/connectors/osmosis/mocks/positionsOwned-GAMM-in.json b/test/connectors/osmosis/mocks/positionsOwned-GAMM-in.json new file mode 100644 index 0000000000..e995b99bfd --- /dev/null +++ b/test/connectors/osmosis/mocks/positionsOwned-GAMM-in.json @@ -0,0 +1,5 @@ +{ + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "poolType": "amm" +} diff --git a/test/connectors/osmosis/mocks/positionsOwned-GAMM-out.json b/test/connectors/osmosis/mocks/positionsOwned-GAMM-out.json new file mode 100644 index 0000000000..0ef4251028 --- /dev/null +++ b/test/connectors/osmosis/mocks/positionsOwned-GAMM-out.json @@ -0,0 +1,13 @@ +{ + "0": { + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "poolAddress": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t", + "baseTokenAmount": 0, + "quoteTokenAmount": 0, + "baseTokenAddress": "", + "quoteTokenAddress": "", + "price": 0.0007959260951650942, + "lpTokenAmount": 121988641470936 + }, + "network": "testnet" +} diff --git a/test/connectors/osmosis/mocks/quotePosition-CLMM-in.json b/test/connectors/osmosis/mocks/quotePosition-CLMM-in.json new file mode 100644 index 0000000000..e1671d58ac --- /dev/null +++ b/test/connectors/osmosis/mocks/quotePosition-CLMM-in.json @@ -0,0 +1,9 @@ +{ + "poolAddress": "osmo1rdm79d008fel4ppkgdcf8pgjwazf72sjfhpyx5kpzlck86slpjusek2en6", + "lowerPrice": 200, + "upperPrice": 1000, + "baseTokenAmount": 0.0002, + "quoteTokenAmount": 0.1, + "network": "testnet", + "slippagePct": 99 +} diff --git a/test/connectors/osmosis/mocks/quotePosition-CLMM-out.json b/test/connectors/osmosis/mocks/quotePosition-CLMM-out.json new file mode 100644 index 0000000000..3b9553e3a2 --- /dev/null +++ b/test/connectors/osmosis/mocks/quotePosition-CLMM-out.json @@ -0,0 +1,8 @@ +{ + "baseLimited": false, + "baseTokenAmount": 0.0002, + "quoteTokenAmount": 0.1, + "baseTokenAmountMax": 0.0002, + "quoteTokenAmountMax": 0.1, + "liquidity": 0 +} diff --git a/test/connectors/osmosis/mocks/quoteSwap-CLMM-in.json b/test/connectors/osmosis/mocks/quoteSwap-CLMM-in.json new file mode 100644 index 0000000000..94888d1e3c --- /dev/null +++ b/test/connectors/osmosis/mocks/quoteSwap-CLMM-in.json @@ -0,0 +1,8 @@ +{ + "slippagePct": 99, + "network": "testnet", + "quoteToken": "ION", + "baseToken": "OSMO", + "amount": 0.001, + "side": "BUY" +} diff --git a/test/connectors/osmosis/mocks/quoteSwap-CLMM-out.json b/test/connectors/osmosis/mocks/quoteSwap-CLMM-out.json new file mode 100644 index 0000000000..b237acbda4 --- /dev/null +++ b/test/connectors/osmosis/mocks/quoteSwap-CLMM-out.json @@ -0,0 +1,12 @@ +{ + "priceImpactPct": 0, + "slippagePct": 99, + "amountIn": 1000, + "amountOut": 1.9450916686006, + "tokenIn": "OSMO", + "tokenOut": "ION", + "price": 0.000019450916686006, + "poolAddress": "1269", + "minAmountOut": 0.019450916686006, + "maxAmountIn": 1000 +} diff --git a/test/connectors/osmosis/mocks/quoteSwap-GAMM-in.json b/test/connectors/osmosis/mocks/quoteSwap-GAMM-in.json new file mode 100644 index 0000000000..94888d1e3c --- /dev/null +++ b/test/connectors/osmosis/mocks/quoteSwap-GAMM-in.json @@ -0,0 +1,8 @@ +{ + "slippagePct": 99, + "network": "testnet", + "quoteToken": "ION", + "baseToken": "OSMO", + "amount": 0.001, + "side": "BUY" +} diff --git a/test/connectors/osmosis/mocks/quoteSwap-GAMM-out.json b/test/connectors/osmosis/mocks/quoteSwap-GAMM-out.json new file mode 100644 index 0000000000..909dbb0419 --- /dev/null +++ b/test/connectors/osmosis/mocks/quoteSwap-GAMM-out.json @@ -0,0 +1,12 @@ +{ + "priceImpactPct": 0.06123486628643313, + "slippagePct": 99, + "amountIn": 1000, + "amountOut": 1.9450916686006, + "tokenIn": "OSMO", + "tokenOut": "ION", + "price": 0.000019450916686006, + "poolAddress": "1", + "minAmountOut": 0.019450916686006, + "maxAmountIn": 1000 +} diff --git a/test/connectors/osmosis/mocks/removeLiquidity-CLMM-in.json b/test/connectors/osmosis/mocks/removeLiquidity-CLMM-in.json new file mode 100644 index 0000000000..b60d54053c --- /dev/null +++ b/test/connectors/osmosis/mocks/removeLiquidity-CLMM-in.json @@ -0,0 +1,6 @@ +{ + "network": "testnet", + "positionAddress": "3493", + "percentageToRemove": 50, + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" +} diff --git a/test/connectors/osmosis/mocks/removeLiquidity-CLMM-out.json b/test/connectors/osmosis/mocks/removeLiquidity-CLMM-out.json new file mode 100644 index 0000000000..e65f8cc4a8 --- /dev/null +++ b/test/connectors/osmosis/mocks/removeLiquidity-CLMM-out.json @@ -0,0 +1,9 @@ +{ + "signature": "3F06EF51E9AF9317A000D6AC8B8CFDBE5CBA61E6E0E476D058F6E080E06BACAB", + "status": 0, + "data": { + "fee": 0, + "baseTokenAmountRemoved": 0.024841, + "quoteTokenAmountRemoved": 0 + } +} diff --git a/test/connectors/osmosis/mocks/removeLiquidity-GAMM-all-in.json b/test/connectors/osmosis/mocks/removeLiquidity-GAMM-all-in.json new file mode 100644 index 0000000000..94568c5315 --- /dev/null +++ b/test/connectors/osmosis/mocks/removeLiquidity-GAMM-all-in.json @@ -0,0 +1,6 @@ +{ + "percentageToRemove": 100, + "poolAddress": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t", + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" +} diff --git a/test/connectors/osmosis/mocks/removeLiquidity-GAMM-all-out.json b/test/connectors/osmosis/mocks/removeLiquidity-GAMM-all-out.json new file mode 100644 index 0000000000..b194affd9a --- /dev/null +++ b/test/connectors/osmosis/mocks/removeLiquidity-GAMM-all-out.json @@ -0,0 +1,9 @@ +{ + "signature": "5A0D7550E67ADB8A9E584799520F1BC764AE0A47A7B14BB85708656BEB1E2A3D", + "status": 0, + "data": { + "fee": 0, + "baseTokenAmountRemoved": 0.001992, + "quoteTokenAmountRemoved": 2.495143 + } +} diff --git a/test/connectors/osmosis/mocks/removeLiquidity-GAMM-partial-in.json b/test/connectors/osmosis/mocks/removeLiquidity-GAMM-partial-in.json new file mode 100644 index 0000000000..41f6566503 --- /dev/null +++ b/test/connectors/osmosis/mocks/removeLiquidity-GAMM-partial-in.json @@ -0,0 +1,6 @@ +{ + "percentageToRemove": 20, + "poolAddress": "osmo1mw0ac6rwlp5r8wapwk3zs6g29h8fcscxqakdzw9emkne6c8wjp9q0t3v8t", + "network": "testnet", + "walletAddress": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" +} diff --git a/test/connectors/osmosis/mocks/removeLiquidity-GAMM-partial-out.json b/test/connectors/osmosis/mocks/removeLiquidity-GAMM-partial-out.json new file mode 100644 index 0000000000..ea395a9e04 --- /dev/null +++ b/test/connectors/osmosis/mocks/removeLiquidity-GAMM-partial-out.json @@ -0,0 +1,9 @@ +{ + "signature": "BFB3830DCF1CAC0A467B748AC006EA77148C24633DD903E95E412F545664B5F1", + "status": 0, + "data": { + "fee": 0, + "baseTokenAmountRemoved": 0.000003, + "quoteTokenAmountRemoved": -0.002766 + } +} diff --git a/test/connectors/osmosis/mocks/status-out.json b/test/connectors/osmosis/mocks/status-out.json new file mode 100644 index 0000000000..e50277c62b --- /dev/null +++ b/test/connectors/osmosis/mocks/status-out.json @@ -0,0 +1,9 @@ +{ + "chain": "cosmos", + "network": "testnet", + "rpcUrl": "https://rpc.testnet.osmosis.zone/", + "currentBlockNumber": 41884325, + "nativeCurrency": "OSMO", + "rpcProvider": "osmosis", + "swapProvider": "osmosis" +} diff --git a/test/connectors/osmosis/mocks/tokens-OSMO-in.json b/test/connectors/osmosis/mocks/tokens-OSMO-in.json new file mode 100644 index 0000000000..f90ec51027 --- /dev/null +++ b/test/connectors/osmosis/mocks/tokens-OSMO-in.json @@ -0,0 +1,4 @@ +{ + "network": "testnet", + "tokenSymbols": ["OSMO"] +} diff --git a/test/connectors/osmosis/mocks/tokens-OSMO-out.json b/test/connectors/osmosis/mocks/tokens-OSMO-out.json new file mode 100644 index 0000000000..bc689571d3 --- /dev/null +++ b/test/connectors/osmosis/mocks/tokens-OSMO-out.json @@ -0,0 +1,11 @@ +{ + "tokens": [ + { + "chainId": 0, + "address": "uosmo", + "name": "Osmosis Testnet", + "symbol": "OSMO", + "decimals": 6 + } + ] +} diff --git a/test/connectors/osmosis/mocks/tokens-all-in.json b/test/connectors/osmosis/mocks/tokens-all-in.json new file mode 100644 index 0000000000..1ec0bef83c --- /dev/null +++ b/test/connectors/osmosis/mocks/tokens-all-in.json @@ -0,0 +1,4 @@ +{ + "network": "testnet", + "tokenSymbols": [] +} diff --git a/test/connectors/osmosis/mocks/tokens-all-out.json b/test/connectors/osmosis/mocks/tokens-all-out.json new file mode 100644 index 0000000000..3d713aad42 --- /dev/null +++ b/test/connectors/osmosis/mocks/tokens-all-out.json @@ -0,0 +1,18 @@ +{ + "tokens": [ + { + "chainId": 0, + "address": "uosmo", + "name": "Osmosis Testnet", + "symbol": "OSMO", + "decimals": 6 + }, + { + "chainId": 0, + "address": "uion", + "name": "Ion", + "symbol": "ION", + "decimals": 6 + } + ] +} diff --git a/test/connectors/osmosis/mocks/transfer-in.json b/test/connectors/osmosis/mocks/transfer-in.json new file mode 100644 index 0000000000..293805b7e1 --- /dev/null +++ b/test/connectors/osmosis/mocks/transfer-in.json @@ -0,0 +1,8 @@ +{ + "from": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs", + "to": "osmo1mvsg3en5ulpnpd3dset2m86zjpnzp4v4epmjh7", + "token": "OSMO", + "amount": "0.00001", + "chains": "cosmos", + "network": "testnet" +} diff --git a/test/connectors/osmosis/mocks/transfer-out.json b/test/connectors/osmosis/mocks/transfer-out.json new file mode 100644 index 0000000000..8418ee63e4 --- /dev/null +++ b/test/connectors/osmosis/mocks/transfer-out.json @@ -0,0 +1 @@ +"Transfer success. To: osmo1mvsg3en5ulpnpd3dset2m86zjpnzp4v4epmjh7 From: osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs Hash: 426D2164B9494B9EFA4AE6B30F915D9B7BDEB4062C1C89D4C80372498D38D159 gasUsed: 89901 gasWanted: 146030" diff --git a/test/connectors/osmosis/mocks/wallet-balances-ALL-in.json b/test/connectors/osmosis/mocks/wallet-balances-ALL-in.json new file mode 100644 index 0000000000..7f6e215aa2 --- /dev/null +++ b/test/connectors/osmosis/mocks/wallet-balances-ALL-in.json @@ -0,0 +1,145 @@ +{ + "member": { + "privkey": { + "0": 46, + "1": 139, + "2": 233, + "3": 134, + "4": 247, + "5": 47, + "6": 118, + "7": 219, + "8": 167, + "9": 248, + "10": 68, + "11": 139, + "12": 46, + "13": 35, + "14": 66, + "15": 211, + "16": 41, + "17": 124, + "18": 214, + "19": 40, + "20": 207, + "21": 8, + "22": 170, + "23": 217, + "24": 185, + "25": 0, + "26": 152, + "27": 16, + "28": 40, + "29": 36, + "30": 249, + "31": 213 + }, + "pubkey": { + "0": 3, + "1": 204, + "2": 105, + "3": 229, + "4": 145, + "5": 67, + "6": 172, + "7": 70, + "8": 197, + "9": 228, + "10": 168, + "11": 37, + "12": 108, + "13": 111, + "14": 151, + "15": 92, + "16": 113, + "17": 124, + "18": 44, + "19": 239, + "20": 122, + "21": 248, + "22": 26, + "23": 85, + "24": 218, + "25": 77, + "26": 44, + "27": 132, + "28": 90, + "29": 174, + "30": 144, + "31": 79, + "32": 130 + }, + "prefix": "osmo" + }, + "pubkey": { + "0": 3, + "1": 204, + "2": 105, + "3": 229, + "4": 145, + "5": 67, + "6": 172, + "7": 70, + "8": 197, + "9": 228, + "10": 168, + "11": 37, + "12": 108, + "13": 111, + "14": 151, + "15": 92, + "16": 113, + "17": 124, + "18": 44, + "19": 239, + "20": 122, + "21": 248, + "22": 26, + "23": 85, + "24": 218, + "25": 77, + "26": 44, + "27": 132, + "28": 90, + "29": 174, + "30": 144, + "31": 79, + "32": 130 + }, + "privkey": { + "0": 46, + "1": 139, + "2": 233, + "3": 134, + "4": 247, + "5": 47, + "6": 118, + "7": 219, + "8": 167, + "9": 248, + "10": 68, + "11": 139, + "12": 46, + "13": 35, + "14": 66, + "15": 211, + "16": 41, + "17": 124, + "18": 214, + "19": 40, + "20": 207, + "21": 8, + "22": 170, + "23": 217, + "24": 185, + "25": 0, + "26": 152, + "27": 16, + "28": 40, + "29": 36, + "30": 249, + "31": 213 + }, + "prefix": "osmo", + "address": "osmo1gxfandcf6x6y0lv3afv0p4w4akv809ycrly4cs" +} diff --git a/test/connectors/osmosis/mocks/wallet-balances-ALL-out.json b/test/connectors/osmosis/mocks/wallet-balances-ALL-out.json new file mode 100644 index 0000000000..850e2ed9e3 --- /dev/null +++ b/test/connectors/osmosis/mocks/wallet-balances-ALL-out.json @@ -0,0 +1,18 @@ +{ + "gamm/pool/62": { + "value": "36022031254003350", + "decimals": 6 + }, + "ATOM": { + "value": "40924", + "decimals": 6 + }, + "ION": { + "value": "93777", + "decimals": 6 + }, + "OSMO": { + "value": "152046201", + "decimals": 6 + } +} diff --git a/test/mocks/shared-mocks.ts b/test/mocks/shared-mocks.ts index fba7b6a777..5a549d551e 100644 --- a/test/mocks/shared-mocks.ts +++ b/test/mocks/shared-mocks.ts @@ -32,6 +32,15 @@ export const mockConfigStorage: Record = { 'ethereum-mainnet.nativeCurrencySymbol': 'ETH', 'ethereum-goerli.nodeURL': 'https://goerli.infura.io/v3/test', 'ethereum-goerli.nativeCurrencySymbol': 'ETH', + // Cosmos configs + 'cosmos-mainnet.nodeURL': 'https://cosmos-rpc.publicnode.com:443', + 'cosmos-mainnet.chainName': 'cosmoshub-4', + 'cosmos-testnet.nodeURL': 'https://cosmos-testnet-rpc.polkachu.com', + 'cosmos-testnet.chainName': 'theta-testnet-001', + 'cosmos-osmosis-mainnet.nodeURL': 'https://rpc.osmosis.zone/', + 'cosmos-osmosis-mainnet.chainName': 'osmosis-1', + 'cosmos-osmosis-testnet.nodeURL': 'https://rpc.testnet.osmosis.zone/', + 'cosmos-osmosis-testnet.chainName': 'osmo-test-5', // Connector configurations 'jupiter.slippagePct': 1, 'jupiter.priorityLevel': 'medium', @@ -55,6 +64,10 @@ export const mockConfigManagerV2 = { 'ethereum-goerli': {}, 'solana-mainnet-beta': {}, 'solana-devnet': {}, + 'cosmos-mainnet': {}, + 'cosmos-testnet': {}, + 'cosmos-osmosis-mainnet': {}, + 'cosmos-osmosis-testnet': {}, uniswap: {}, jupiter: {}, meteora: {}, @@ -140,6 +153,12 @@ export const mockEthereumChainConfig = { rpcProvider: 'url', }; +export const mockCosmosChainConfig = { + defaultNetwork: 'osmosis-mainnet', + defaultWallet: 'test-wallet', + rpcProvider: 'url', +}; + // Setup all common mocks export function setupCommonMocks(options: { skipLogger?: boolean } = {}) { // Mock logger only if not skipped @@ -175,6 +194,11 @@ export function setupCommonMocks(options: { skipLogger?: boolean } = {}) { getEthereumChainConfig: jest.fn().mockReturnValue(mockEthereumChainConfig), })); + jest.mock('../../src/chains/cosmos/cosmos.config', () => ({ + ...jest.requireActual('../../src/chains/cosmos/cosmos.config'), + getCosmosChainConfig: jest.fn().mockReturnValue(mockCosmosChainConfig), + })); + // Mock fs for token lists jest.mock('fs', () => { const actualFs = jest.requireActual('fs'); @@ -221,6 +245,14 @@ export function resetAllMocks() { 'ethereum-mainnet.nativeCurrencySymbol': 'ETH', 'ethereum-goerli.nodeURL': 'https://goerli.infura.io/v3/test', 'ethereum-goerli.nativeCurrencySymbol': 'ETH', + 'cosmos-mainnet.nodeURL': 'https://cosmos-rpc.publicnode.com:443', + 'cosmos-mainnet.chainName': 'cosmoshub-4', + 'cosmos-testnet.nodeURL': 'https://cosmos-testnet-rpc.polkachu.com', + 'cosmos-testnet.chainName': 'theta-testnet-001', + 'cosmos-osmosis-mainnet.nodeURL': 'https://rpc.osmosis.zone/', + 'cosmos-osmosis-mainnet.chainName': 'osmosis-1', + 'cosmos-osmosis-testnet.nodeURL': 'https://rpc.testnet.osmosis.zone/', + 'cosmos-osmosis-testnet.chainName': 'osmo-test-5', 'jupiter.slippagePct': 1, 'jupiter.priorityLevel': 'medium', 'jupiter.apiKey': undefined, diff --git a/tsconfig.json b/tsconfig.json index 90e58d2f49..1be0d7f851 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,9 @@ "compilerOptions": { "strict": false, "target": "ESNext", - "module": "CommonJS", + "module": "nodenext", + "isolatedModules": true, + "moduleResolution": "nodenext", "allowJs": true, "inlineSourceMap": true, "removeComments": true, @@ -19,6 +21,7 @@ "experimentalDecorators": true, "resolveJsonModule": true, "baseUrl": ".", + "rootDir": ".", "paths": { "#src": ["src/index.ts"], "#src/*": ["src/*"],