From 0d96efe413b8675971991b9eb9386de1a175d3b0 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Tue, 25 Nov 2025 13:28:31 -0300 Subject: [PATCH 1/2] feat: creating the coingecko mock API; feat: configuring docker-compose.offline.yml file to start coingecko mock and add it to the price estimators; --- .../src/price_estimation/native/coingecko.rs | 4 +- playground/.env.offline | 3 + playground/docker-compose.offline.yml | 30 +++- playground/mocks/README.md | 54 ++++++ playground/mocks/coingecko/.dockerignore | 6 + playground/mocks/coingecko/.gitignore | 5 + playground/mocks/coingecko/Dockerfile | 22 +++ playground/mocks/coingecko/README.md | 118 +++++++++++++ playground/mocks/coingecko/package.json | 23 +++ playground/mocks/coingecko/src/index.ts | 158 ++++++++++++++++++ .../mocks/coingecko/src/tokens.config.ts | 84 ++++++++++ playground/mocks/coingecko/tsconfig.json | 17 ++ 12 files changed, 519 insertions(+), 5 deletions(-) create mode 100644 playground/mocks/README.md create mode 100644 playground/mocks/coingecko/.dockerignore create mode 100644 playground/mocks/coingecko/.gitignore create mode 100644 playground/mocks/coingecko/Dockerfile create mode 100644 playground/mocks/coingecko/README.md create mode 100644 playground/mocks/coingecko/package.json create mode 100644 playground/mocks/coingecko/src/index.ts create mode 100644 playground/mocks/coingecko/src/tokens.config.ts create mode 100644 playground/mocks/coingecko/tsconfig.json diff --git a/crates/shared/src/price_estimation/native/coingecko.rs b/crates/shared/src/price_estimation/native/coingecko.rs index 20fc412120..a7fddc0bcc 100644 --- a/crates/shared/src/price_estimation/native/coingecko.rs +++ b/crates/shared/src/price_estimation/native/coingecko.rs @@ -83,7 +83,9 @@ impl CoinGecko { Chain::Lens => "lens".to_string(), Chain::Linea => "linea".to_string(), Chain::Plasma => "plasma".to_string(), - Chain::Sepolia | Chain::Goerli | Chain::Hardhat => { + // Hardhat/Anvil is a local Ethereum fork, use ethereum pricing for offline development + Chain::Hardhat => "ethereum".to_string(), + Chain::Sepolia | Chain::Goerli => { anyhow::bail!("unsupported network {}", chain.name()) } }; diff --git a/playground/.env.offline b/playground/.env.offline index 7f494d7401..dc2fea1e3b 100644 --- a/playground/.env.offline +++ b/playground/.env.offline @@ -18,6 +18,9 @@ NATIVE_TOKEN_ADDRESS=0xb3af08c783c4d9c380893257980b5e26657f2317 UNISWAP_V2_FACTORY_ADDRESS=0x7fb9dcdea3bec40d02e25ae230a64d7e8ddaa304 UNISWAP_V2_ROUTER_ADDRESS=0x6c2014489c8479a8a36be65e3ccad07fa3cec029 +# Price Estimation Configuration +COIN_GECKO_URL=http://coingecko-mock:3000/api/v3/simple/token_price + # CoW Protocol Addresses (from deployment) SETTLEMENT_CONTRACT_ADDRESS=0x5113d53be0167ad02f9db0e415e210f11a6cb2ba AUTHENTICATOR_ADDRESS=0x5fe5f7f937b63189bd0d33d78b264d4608973c8c diff --git a/playground/docker-compose.offline.yml b/playground/docker-compose.offline.yml index ffa5c45d36..228508b1f3 100644 --- a/playground/docker-compose.offline.yml +++ b/playground/docker-compose.offline.yml @@ -31,6 +31,23 @@ services: retries: 10 start_period: 2s + # Mock Coingecko API for price fetching in offline mode + coingecko-mock: + build: + context: ./mocks/coingecko + dockerfile: Dockerfile + restart: always + environment: + - PORT=3000 + ports: + - 3001:3000 + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/health || exit 1"] + interval: 5s + timeout: 3s + retries: 5 + start_period: 5s + db: image: postgres:16 restart: always @@ -87,7 +104,7 @@ services: - NODE_URL=http://chain:8545 - DB_WRITE_URL=postgres://db:5432/?user=${POSTGRES_USER}&password=${POSTGRES_PASSWORD} - DB_READ_URL=postgres://db:5432/?user=${POSTGRES_USER}&password=${POSTGRES_PASSWORD} - - LOG_FILTER=orderbook=trace,shared=trace + - LOG_FILTER=orderbook=trace,shared=trace,shared::price_estimation=trace,shared::price_estimation::native=trace - ACCOUNT_BALANCES_SIMULATION=false - ACCOUNT_BALANCES_SIMULATOR=Web3 - SIMULATION_NODE_URL=http://chain:8545 @@ -95,7 +112,8 @@ services: - ENABLE_EIP1271_ORDERS=true - PRICE_ESTIMATORS=None - PRICE_ESTIMATION_DRIVERS=baseline|http://driver/baseline - - NATIVE_PRICE_ESTIMATORS=baseline|http://driver/baseline + - NATIVE_PRICE_ESTIMATORS=baseline|http://driver/baseline,CoinGecko + - COIN_GECKO_URL=http://coingecko-mock:3000/api/v3/simple/token_price - DRIVERS=baseline|http://driver/baseline - BIND_ADDRESS=0.0.0.0:80 - BASELINE_SOURCES=UniswapV2 @@ -115,6 +133,8 @@ services: condition: service_completed_successfully chain: condition: service_healthy + coingecko-mock: + condition: service_healthy ports: - 8080:80 # API - 9586:9586 # metrics @@ -153,14 +173,14 @@ services: - SETTLE_INTERVAL=15s - GAS_ESTIMATORS=Native,Web3 - PRICE_ESTIMATORS=None - - NATIVE_PRICE_ESTIMATORS=baseline|http://driver/baseline + - NATIVE_PRICE_ESTIMATORS=baseline|http://driver/baseline,CoinGecko + - COIN_GECKO_URL=http://coingecko-mock:3000/api/v3/simple/token_price - BLOCK_STREAM_POLL_INTERVAL=1s - NATIVE_PRICE_CACHE_MAX_UPDATE_SIZE=100 - NATIVE_PRICE_CACHE_MAX_AGE=20m - SOLVER_TIME_LIMIT=5 - PRICE_ESTIMATION_DRIVERS=baseline|http://driver/baseline - RUN_LOOP_NATIVE_PRICE_TIMEOUT=5s - - NATIVE_PRICE_ESTIMATORS=baseline|http://driver/baseline - NATIVE_PRICE_ESTIMATION_RESULTS_REQUIRED=1 - DRIVERS=baseline|http://driver/baseline|0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 - BASELINE_SOURCES=UniswapV2 @@ -181,6 +201,8 @@ services: condition: service_healthy chain: condition: service_healthy + coingecko-mock: + condition: service_healthy ports: - 9589:9589 # metrics - 6670:6669 # tokio console diff --git a/playground/mocks/README.md b/playground/mocks/README.md new file mode 100644 index 0000000000..30d51d00fc --- /dev/null +++ b/playground/mocks/README.md @@ -0,0 +1,54 @@ +# Mock Services + +This directory contains mock services used for offline development and testing of the CoW Protocol playground. + +## Available Mocks + +### Coingecko (`./coingecko/`) + +Mock implementation of the Coingecko API for price fetching in offline mode. + +- **Technology**: Node.js + TypeScript + Hono API +- **Endpoint**: `/api/v3/simple/token_price/ethereum` +- **Purpose**: Provides configurable token prices in ETH denomination without requiring external API access +- **Configuration**: Token prices can be configured in `./coingecko/src/tokens.config.ts` + +**Supported Tokens** (offline mode): +- WETH +- DAI +- USDC +- USDT +- GNO + +See `./coingecko/README.md` for detailed documentation. + +## Architecture + +Mock services are organized at the playground level to: +- Maintain clear separation from deployment infrastructure (`offline-mode/`) +- Improve discoverability of available mocks +- Enable easy addition of new mock services (e.g., block explorer, subgraph, etc.) +- Support reusability across different playground configurations + +## Adding New Mocks + +To add a new mock service: + +1. Create a new directory: `mocks//` +2. Implement the mock service with a Dockerfile +3. Add the service to `docker-compose.offline.yml` +4. Update this README with documentation + +## Usage + +Mock services are automatically started when running the offline playground: + +```bash +docker-compose -f docker-compose.offline.yml up +``` + +Individual mocks can be built and run separately: + +```bash +docker-compose -f docker-compose.offline.yml up coingecko-mock +``` diff --git a/playground/mocks/coingecko/.dockerignore b/playground/mocks/coingecko/.dockerignore new file mode 100644 index 0000000000..10b740cfb8 --- /dev/null +++ b/playground/mocks/coingecko/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +*.log +.env +.DS_Store +README.md diff --git a/playground/mocks/coingecko/.gitignore b/playground/mocks/coingecko/.gitignore new file mode 100644 index 0000000000..397b0d9850 --- /dev/null +++ b/playground/mocks/coingecko/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.log +.env +.DS_Store diff --git a/playground/mocks/coingecko/Dockerfile b/playground/mocks/coingecko/Dockerfile new file mode 100644 index 0000000000..2060f30697 --- /dev/null +++ b/playground/mocks/coingecko/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY tsconfig.json ./ + +# Install dependencies +RUN npm install + +# Copy source code +COPY src ./src + +# Build TypeScript +RUN npm run build + +# Expose port +EXPOSE 3000 + +# Start the server +CMD ["npm", "start"] diff --git a/playground/mocks/coingecko/README.md b/playground/mocks/coingecko/README.md new file mode 100644 index 0000000000..2049f2b35f --- /dev/null +++ b/playground/mocks/coingecko/README.md @@ -0,0 +1,118 @@ +# Coingecko Mock API + +Mock implementation of the Coingecko API for offline development and testing. + +## Overview + +This service provides a lightweight mock of the Coingecko API, specifically the `/api/v3/simple/token_price` endpoint. It returns configurable prices for the 5 tokens deployed in offline mode. + +## Supported Tokens + +The following tokens are supported (configured in `src/tokens.config.ts`): + +| Symbol | Address | Price (ETH) | +|--------|---------|-------------| +| WETH | `0xb3af08c783c4d9c380893257980b5e26657f2317` | 1.0 | +| DAI | `0xb12812c0cad46d18b669b31059d485fe90b1a839` | 0.0004 | +| USDC | `0xb04afbcd351a0a7e4ff658b3772ee5f3f5b6e4ae` | 0.0004 | +| USDT | `0x171a30524fd943df1a12cbb9da291bf4e34ac84b` | 0.0004 | +| GNO | `0x51a53858a4a8b81814da35c4604eb9003d56a895` | 0.05 | + +## API Endpoints + +### Price Fetching + +```bash +GET /api/v3/simple/token_price/ethereum?contract_addresses=&vs_currencies=eth&precision=full +``` + +**Query Parameters:** +- `contract_addresses`: Comma-separated list of token contract addresses +- `vs_currencies`: Currency denomination (only `eth` is supported) +- `precision`: Precision level (e.g., `full`) + +**Example Request:** +```bash +curl "http://localhost:3000/api/v3/simple/token_price/ethereum?contract_addresses=0xb3af08c783c4d9c380893257980b5e26657f2317,0xb12812c0cad46d18b669b31059d485fe90b1a839&vs_currencies=eth&precision=full" +``` + +**Example Response:** +```json +{ + "0xb3af08c783c4d9c380893257980b5e26657f2317": { + "eth": 1.0 + }, + "0xb12812c0cad46d18b669b31059d485fe90b1a839": { + "eth": 0.0004 + } +} +``` + +### Health Check + +```bash +GET /health +``` + +Returns the service status. + +### List Supported Tokens + +```bash +GET /api/v3/tokens +``` + +Returns a list of all supported token addresses. + +## Local Development + +### Prerequisites + +- Node.js 20+ +- npm or yarn + +### Installation + +```bash +npm install +``` + +### Running + +```bash +# Development mode with hot reload +npm run dev + +# Production build +npm run build +npm start +``` + +### Environment Variables + +- `PORT`: Server port (default: 3000) + +## Docker + +### Build + +```bash +docker build -t coingecko-mock . +``` + +### Run + +```bash +docker run -p 3000:3000 coingecko-mock +``` + +## Configuration + +To add or modify token prices, edit `src/tokens.config.ts` and update the `TOKENS` object. + +## Limitations + +- Only supports Ethereum platform (`ethereum`) +- Only supports ETH denomination (`vs_currencies=eth`) +- Only returns prices for tokens defined in `tokens.config.ts` +- Does not support historical prices, market caps, or other Coingecko features diff --git a/playground/mocks/coingecko/package.json b/playground/mocks/coingecko/package.json new file mode 100644 index 0000000000..bfa3f47f43 --- /dev/null +++ b/playground/mocks/coingecko/package.json @@ -0,0 +1,23 @@ +{ + "name": "coingecko-mock", + "version": "1.0.0", + "description": "Mock Coingecko API for offline development", + "main": "dist/index.js", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + }, + "keywords": ["coingecko", "mock", "api"], + "author": "", + "license": "MIT", + "dependencies": { + "hono": "^4.0.0", + "@hono/node-server": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } +} diff --git a/playground/mocks/coingecko/src/index.ts b/playground/mocks/coingecko/src/index.ts new file mode 100644 index 0000000000..d59d4c8586 --- /dev/null +++ b/playground/mocks/coingecko/src/index.ts @@ -0,0 +1,158 @@ +import { Hono } from 'hono'; +import { serve } from '@hono/node-server'; +import { getTokenPrice, getSupportedTokens } from './tokens.config'; + +const app = new Hono(); + +// Health check endpoint +app.get('/health', (c) => { + return c.json({ + status: 'ok', + service: 'coingecko-mock', + timestamp: new Date().toISOString(), + }); +}); + +// List all supported tokens +app.get('/api/v3/tokens', (c) => { + return c.json({ + supported_tokens: getSupportedTokens(), + }); +}); + +/** + * Mock Coingecko API endpoint: /api/v3/simple/token_price/:platform + * + * Query parameters: + * - contract_addresses: Comma-separated list of token contract addresses + * - vs_currencies: Currency to price against (e.g., "eth") + * - precision: Number precision (e.g., "full") + * + * Example: + * GET /api/v3/simple/token_price/ethereum?contract_addresses=0xabc,0xdef&vs_currencies=eth&precision=full + */ +app.get('/api/v3/simple/token_price/:platform', (c) => { + const platform = c.req.param('platform'); + const contractAddresses = c.req.query('contract_addresses'); + const vsCurrencies = c.req.query('vs_currencies'); + const precision = c.req.query('precision'); + + // Log the request for debugging + console.log(`[${new Date().toISOString()}] GET /api/v3/simple/token_price/${platform}`); + console.log(` contract_addresses: ${contractAddresses}`); + console.log(` vs_currencies: ${vsCurrencies}`); + console.log(` precision: ${precision}`); + + // Validate required parameters + if (!contractAddresses) { + return c.json( + { + error: 'contract_addresses parameter is required', + }, + 400 + ); + } + + if (!vsCurrencies) { + return c.json( + { + error: 'vs_currencies parameter is required', + }, + 400 + ); + } + + // Only support Ethereum platform in offline mode + if (platform !== 'ethereum') { + return c.json( + { + error: `Platform "${platform}" is not supported in offline mode. Only "ethereum" is supported.`, + }, + 400 + ); + } + + // Only support ETH as the denomination currency + if (vsCurrencies !== 'eth') { + return c.json( + { + error: `Currency "${vsCurrencies}" is not supported. Only "eth" is supported in offline mode.`, + }, + 400 + ); + } + + // Parse contract addresses + const addresses = contractAddresses.split(',').map((addr) => addr.trim().toLowerCase()); + + // Build response object matching Coingecko's format + const response: Record = {}; + + for (const address of addresses) { + const price = getTokenPrice(address); + + if (price !== null) { + // Token is supported, return price + response[address] = { + eth: price, + }; + console.log(` ✓ ${address}: ${price} ETH`); + } else { + // Token not supported, return empty object (Coingecko behavior) + response[address] = {}; + console.log(` ✗ ${address}: not supported`); + } + } + + console.log(''); + return c.json(response); +}); + +// 404 handler +app.notFound((c) => { + return c.json( + { + error: 'Not Found', + message: `Endpoint ${c.req.path} not found`, + hint: 'This is a mock Coingecko API. Only /api/v3/simple/token_price/:platform is supported.', + }, + 404 + ); +}); + +// Error handler +app.onError((err, c) => { + console.error(`[ERROR] ${err.message}`); + return c.json( + { + error: 'Internal Server Error', + message: err.message, + }, + 500 + ); +}); + +// Start server +const port = parseInt(process.env.PORT || '3000', 10); + +console.log(` +╔═══════════════════════════════════════════════════╗ +║ ║ +║ 🦎 Coingecko Mock API - Offline Mode ║ +║ ║ +║ Port: ${port.toString().padEnd(43, ' ')}║ +║ Platform: ethereum ║ +║ Currency: eth ║ +║ ║ +║ Endpoints: ║ +║ - GET /health ║ +║ - GET /api/v3/tokens ║ +║ - GET /api/v3/simple/token_price/ethereum ║ +║ ║ +╚═══════════════════════════════════════════════════╝ +`); + +serve({ + fetch: app.fetch, + port, +}); diff --git a/playground/mocks/coingecko/src/tokens.config.ts b/playground/mocks/coingecko/src/tokens.config.ts new file mode 100644 index 0000000000..3257b69124 --- /dev/null +++ b/playground/mocks/coingecko/src/tokens.config.ts @@ -0,0 +1,84 @@ +/** + * Token configuration for Coingecko mock API + * + * Prices are denominated in ETH as per Coingecko's API format. + * These are the 5 tokens deployed in offline mode with deterministic addresses. + */ + +export interface TokenConfig { + address: string; + symbol: string; + name: string; + decimals: number; + priceInEth: number; +} + +export const TOKENS: Record = { + // WETH - Wrapped Ether + '0xb3af08c783c4d9c380893257980b5e26657f2317': { + address: '0xb3af08c783c4d9c380893257980b5e26657f2317', + symbol: 'WETH', + name: 'Wrapped Ether', + decimals: 18, + priceInEth: 1.0, // 1 WETH = 1 ETH by definition + }, + + // DAI - Dai Stablecoin + '0xb12812c0cad46d18b669b31059d485fe90b1a839': { + address: '0xb12812c0cad46d18b669b31059d485fe90b1a839', + symbol: 'DAI', + name: 'Dai Stablecoin', + decimals: 18, + priceInEth: 0.0004, // Assuming 1 DAI ≈ $1 and 1 ETH ≈ $2500 + }, + + // USDC - USD Coin + '0xb04afbcd351a0a7e4ff658b3772ee5f3f5b6e4ae': { + address: '0xb04afbcd351a0a7e4ff658b3772ee5f3f5b6e4ae', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + priceInEth: 0.0004, // Assuming 1 USDC ≈ $1 and 1 ETH ≈ $2500 + }, + + // USDT - Tether USD + '0x171a30524fd943df1a12cbb9da291bf4e34ac84b': { + address: '0x171a30524fd943df1a12cbb9da291bf4e34ac84b', + symbol: 'USDT', + name: 'Tether USD', + decimals: 6, + priceInEth: 0.0004, // Assuming 1 USDT ≈ $1 and 1 ETH ≈ $2500 + }, + + // GNO - Gnosis Token + '0x51a53858a4a8b81814da35c4604eb9003d56a895': { + address: '0x51a53858a4a8b81814da35c4604eb9003d56a895', + symbol: 'GNO', + name: 'Gnosis Token', + decimals: 18, + priceInEth: 0.05, // Assuming 1 GNO ≈ $125 and 1 ETH ≈ $2500 + }, +}; + +/** + * Get token price by address (case-insensitive) + */ +export function getTokenPrice(address: string): number | null { + const normalizedAddress = address.toLowerCase(); + const token = TOKENS[normalizedAddress]; + return token ? token.priceInEth : null; +} + +/** + * Check if token is supported + */ +export function isTokenSupported(address: string): boolean { + return address.toLowerCase() in TOKENS; +} + +/** + * Get all supported token addresses + */ +export function getSupportedTokens(): string[] { + return Object.keys(TOKENS); +} diff --git a/playground/mocks/coingecko/tsconfig.json b/playground/mocks/coingecko/tsconfig.json new file mode 100644 index 0000000000..c880eacf0b --- /dev/null +++ b/playground/mocks/coingecko/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} From 0252bb3f706657d4eab42fe235da4e2d272c2533 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Wed, 26 Nov 2025 18:39:27 -0300 Subject: [PATCH 2/2] refactor: removing unused coin_gecko_url env variable from .env.offline; --- playground/.env.offline | 3 --- 1 file changed, 3 deletions(-) diff --git a/playground/.env.offline b/playground/.env.offline index dc2fea1e3b..7f494d7401 100644 --- a/playground/.env.offline +++ b/playground/.env.offline @@ -18,9 +18,6 @@ NATIVE_TOKEN_ADDRESS=0xb3af08c783c4d9c380893257980b5e26657f2317 UNISWAP_V2_FACTORY_ADDRESS=0x7fb9dcdea3bec40d02e25ae230a64d7e8ddaa304 UNISWAP_V2_ROUTER_ADDRESS=0x6c2014489c8479a8a36be65e3ccad07fa3cec029 -# Price Estimation Configuration -COIN_GECKO_URL=http://coingecko-mock:3000/api/v3/simple/token_price - # CoW Protocol Addresses (from deployment) SETTLEMENT_CONTRACT_ADDRESS=0x5113d53be0167ad02f9db0e415e210f11a6cb2ba AUTHENTICATOR_ADDRESS=0x5fe5f7f937b63189bd0d33d78b264d4608973c8c