From e4b55ce8b69b1801ca55b3734aef13a59c31bad8 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:21:34 -0600 Subject: [PATCH 1/6] fix: docs present and matched relative paths --- packages/public-api/Dockerfile | 10 +++++----- packages/public-api/src/assets.ts | 25 +------------------------ 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/packages/public-api/Dockerfile b/packages/public-api/Dockerfile index 7155146d132..fd8e0deff2d 100644 --- a/packages/public-api/Dockerfile +++ b/packages/public-api/Dockerfile @@ -80,12 +80,12 @@ FROM node:22-alpine AS runner WORKDIR /app -# Copy only the bundled server, smoke tests, and asset data (node_modules not needed - esbuild bundles everything) -COPY --from=builder /app/packages/public-api/dist/server.cjs ./server.cjs -COPY --from=builder /app/public/generated/generatedAssetData.json ./public/generated/ +# Copy only the bundled server, docs, and asset data (node_modules not needed - esbuild bundles everything) +COPY --from=builder /app/packages/public-api/dist/server.cjs ./packages/public-api/dist/server.cjs +COPY --from=builder /app/packages/public-api/docs ./packages/public-api/docs +COPY --from=builder /app/public/generated/generatedAssetData.json ./public/generated/generatedAssetData.json # Set environment ENV NODE_ENV=production -ENV ASSET_DATA_PATH=/app/public/generated/generatedAssetData.json -CMD ["node", "server.cjs"] +CMD ["node", "packages/public-api/dist/server.cjs"] diff --git a/packages/public-api/src/assets.ts b/packages/public-api/src/assets.ts index 95f8962a30a..165e68d3740 100644 --- a/packages/public-api/src/assets.ts +++ b/packages/public-api/src/assets.ts @@ -15,30 +15,7 @@ export const initAssets = (): Promise => { console.log('Initializing assets...') try { - // Try to load from the generated asset data file - // First check env var, then relative to cwd, then relative to monorepo root - const possiblePaths = [ - process.env.ASSET_DATA_PATH, - path.join(process.cwd(), 'public/generated/generatedAssetData.json'), - path.join(process.cwd(), '../../public/generated/generatedAssetData.json'), - path.join(process.cwd(), 'generatedAssetData.json'), - ].filter(Boolean) as string[] - - let assetDataPath: string | undefined - for (const p of possiblePaths) { - if (fs.existsSync(p)) { - assetDataPath = p - break - } - } - - if (!assetDataPath) { - const error = new Error( - `Asset data file not found in any of the expected locations: ${possiblePaths.join(', ')}`, - ) - console.warn(error.message) - return Promise.reject(error) - } + const assetDataPath = path.join(__dirname, '../../../public/generated/generatedAssetData.json') const assetDataJson = JSON.parse(fs.readFileSync(assetDataPath, 'utf8')) const localAssetData = assetDataJson.byId From cdaf44647399a1b82ba8d35a8a7c3dec37ade150 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:20:37 -0600 Subject: [PATCH 2/6] remove unecessary config --- packages/public-api/.env.example | 5 ----- packages/public-api/src/config.ts | 4 ---- packages/public-api/src/index.ts | 9 +++++---- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/public-api/.env.example b/packages/public-api/.env.example index 9df9be6ff66..16b06eb6a89 100644 --- a/packages/public-api/.env.example +++ b/packages/public-api/.env.example @@ -1,11 +1,6 @@ # Server Configuration PORT=3001 -HOST=0.0.0.0 NODE_ENV=development -TRUST_PROXY=1 - -# Asset Data Path (optional - will auto-detect if not set) -# ASSET_DATA_PATH=/app/public/generated/generatedAssetData.json # API Configuration (optional - defaults shown) # VITE_PORTALS_API_KEY= diff --git a/packages/public-api/src/config.ts b/packages/public-api/src/config.ts index 4ab2c9f355c..651891a5b73 100644 --- a/packages/public-api/src/config.ts +++ b/packages/public-api/src/config.ts @@ -64,7 +64,3 @@ const getSwapServiceBaseUrl = (): string => { } export const SWAP_SERVICE_BASE_URL = getSwapServiceBaseUrl() - -// API server config -export const API_PORT = parseInt(process.env.PORT || '3001', 10) -export const API_HOST = process.env.HOST || '0.0.0.0' diff --git a/packages/public-api/src/index.ts b/packages/public-api/src/index.ts index 46dab635943..d9abd622280 100644 --- a/packages/public-api/src/index.ts +++ b/packages/public-api/src/index.ts @@ -4,7 +4,6 @@ import cors from 'cors' import express from 'express' import { getAssetsById, initAssets } from './assets' -import { API_HOST, API_PORT } from './config' import { quoteStore } from './lib/quoteStore' import { resolvePartnerCode } from './middleware/auth' import { @@ -23,13 +22,15 @@ import { getRates } from './routes/rates' import { getSwapStatus } from './routes/status' import { initSwapperDeps } from './swapperDeps' +const PORT = process.env.PORT || '3001' + const startServer = async () => { await initAssets() initSwapperDeps(getAssetsById()) const app = express() - app.set('trust proxy', process.env.TRUST_PROXY === '1' ? 1 : false) + app.set('trust proxy', 1) app.use(cors()) app.use(express.json()) @@ -74,8 +75,8 @@ const startServer = async () => { }, ) - app.listen(API_PORT, API_HOST, () => { - console.log(`Public API server running at http://${API_HOST}:${API_PORT}`) + app.listen(PORT, () => { + console.log(`Public API server running on port: ${PORT}`) }) } From 1c5b74127b04fd56aefad5f12c5a5a54459e574c Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:00:25 -0600 Subject: [PATCH 3/6] refine env var handling and usage --- packages/public-api/.env.example | 86 ++++++++++++---- packages/public-api/src/config.ts | 99 +++++++------------ packages/public-api/src/env.ts | 88 +++++++++++++++++ packages/public-api/src/index.ts | 7 +- packages/public-api/src/middleware/auth.ts | 9 +- .../public-api/src/middleware/rateLimit.ts | 26 +++-- .../src/routes/affiliate/getAffiliateStats.ts | 4 +- .../src/routes/status/getSwapStatus.ts | 4 +- .../public-api/src/routes/status/utils.ts | 4 +- packages/public-api/src/swapperDeps.ts | 43 ++++---- 10 files changed, 231 insertions(+), 139 deletions(-) create mode 100644 packages/public-api/src/env.ts diff --git a/packages/public-api/.env.example b/packages/public-api/.env.example index 16b06eb6a89..bcb1999dd70 100644 --- a/packages/public-api/.env.example +++ b/packages/public-api/.env.example @@ -1,23 +1,69 @@ # Server Configuration -PORT=3001 +PORT=3005 NODE_ENV=development -# API Configuration (optional - defaults shown) -# VITE_PORTALS_API_KEY= -# VITE_CHAINALYSIS_API_KEY= -# VITE_ZRX_API_KEY= -# VITE_ZERION_API_KEY= - -# Unchained URLs (defaults to ShapeShift public instances) -# UNCHAINED_ETHEREUM_HTTP_URL=https://api.ethereum.shapeshift.com -# UNCHAINED_THORCHAIN_HTTP_URL=https://api.thorchain.shapeshift.com - -# Rate Limiting (requests per minute per IP, defaults shown) -# RATE_LIMIT_GLOBAL_MAX=300 -# RATE_LIMIT_DATA_MAX=120 -# RATE_LIMIT_SWAP_RATES_MAX=60 -# RATE_LIMIT_SWAP_QUOTE_MAX=45 - -# Partner API Keys (add your partner keys here) -# Format: API_KEY_=:: -# Example: API_KEY_PARTNER1=abc123:MyPartner:50 +# Swap Service +SWAP_SERVICE_BASE_URL=https://dev-api.swap-service.shapeshift.com + +# Unchained URLs +UNCHAINED_ETHEREUM_HTTP_URL=https://dev-api.ethereum.shapeshift.com +UNCHAINED_BITCOIN_HTTP_URL=https://dev-api.bitcoin.shapeshift.com +UNCHAINED_THORCHAIN_HTTP_URL=https://dev-api.thorchain.shapeshift.com +UNCHAINED_MAYACHAIN_HTTP_URL=https://dev-api.mayachain.shapeshift.com +UNCHAINED_COSMOS_HTTP_URL=https://dev-api.cosmos.shapeshift.com +UNCHAINED_AVALANCHE_HTTP_URL=https://dev-api.avalanche.shapeshift.com +UNCHAINED_BNBSMARTCHAIN_HTTP_URL=https://dev-api.bnbsmartchain.shapeshift.com +UNCHAINED_BASE_HTTP_URL=https://dev-api.base.shapeshift.com +UNCHAINED_ARBITRUM_HTTP_URL=https://dev-api.arbitrum.shapeshift.com +UNCHAINED_OPTIMISM_HTTP_URL=https://dev-api.optimism.shapeshift.com +UNCHAINED_POLYGON_HTTP_URL=https://dev-api.polygon.shapeshift.com +UNCHAINED_GNOSIS_HTTP_URL=https://dev-api.gnosis.shapeshift.com +UNCHAINED_DOGECOIN_HTTP_URL=https://dev-api.dogecoin.shapeshift.com +UNCHAINED_LITECOIN_HTTP_URL=https://dev-api.litecoin.shapeshift.com +UNCHAINED_BITCOINCASH_HTTP_URL=https://dev-api.bitcoincash.shapeshift.com + +# Node URLs +THORCHAIN_NODE_URL=https://dev-api.thorchain.shapeshift.com/lcd +MAYACHAIN_NODE_URL=https://api.mayachain.shapeshift.com/lcd +TRON_NODE_URL=https://api.trongrid.io +SUI_NODE_URL=https://fullnode.mainnet.sui.io + +# Midgard URLs +THORCHAIN_MIDGARD_URL=https://dev-api.thorchain.shapeshift.com/midgard/v2 +MAYACHAIN_MIDGARD_URL=https://dev-api.mayachain.shapeshift.com/midgard/v2 + +# Swapper API URLs +COWSWAP_BASE_URL=https://api.cow.fi +PORTALS_BASE_URL=https://api.portals.fi +ZRX_BASE_URL=https://api.proxy.shapeshift.com/api/v1/zrx/ +JUPITER_API_URL=https://quote-api.jup.ag/v6 +RELAY_API_URL=https://api.relay.link +ACROSS_API_URL=https://app.across.to/api +DEBRIDGE_API_URL=https://dln.debridge.finance/v1.0 +CHAINFLIP_API_URL=https://chainflip-broker.io + +# Swapper API Keys (leave empty if not using) +CHAINFLIP_API_KEY= +BEBOP_API_KEY= +NEAR_INTENTS_API_KEY= +TENDERLY_API_KEY= +TENDERLY_ACCOUNT_SLUG= +TENDERLY_PROJECT_SLUG= +ACROSS_INTEGRATOR_ID= + +# Affiliate +DEFAULT_AFFILIATE_BPS=60 + +# Feature Flags +FEATURE_THORCHAINSWAP_LONGTAIL=true +FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL=true +FEATURE_CHAINFLIP_SWAP_DCA=true + +# Rate Limiting (requests per minute per IP) +RATE_LIMIT_GLOBAL_MAX=300 +RATE_LIMIT_DATA_MAX=120 +RATE_LIMIT_SWAP_RATES_MAX=60 +RATE_LIMIT_SWAP_QUOTE_MAX=45 +RATE_LIMIT_SWAP_STATUS_MAX=60 +RATE_LIMIT_AFFILIATE_STATS_MAX=30 +RATE_LIMIT_AFFILIATE_MUTATION_MAX=20 diff --git a/packages/public-api/src/config.ts b/packages/public-api/src/config.ts index 651891a5b73..9bdf84b82ef 100644 --- a/packages/public-api/src/config.ts +++ b/packages/public-api/src/config.ts @@ -1,66 +1,41 @@ import type { SwapperConfig } from '@shapeshiftoss/swapper' -// Server-side config that mirrors the web app's config but from environment variables +import { env } from './env' + export const getServerConfig = (): SwapperConfig => ({ - VITE_UNCHAINED_THORCHAIN_HTTP_URL: - process.env.UNCHAINED_THORCHAIN_HTTP_URL || 'https://api.thorchain.shapeshift.com', - VITE_UNCHAINED_MAYACHAIN_HTTP_URL: - process.env.UNCHAINED_MAYACHAIN_HTTP_URL || 'https://api.mayachain.shapeshift.com', - VITE_UNCHAINED_COSMOS_HTTP_URL: - process.env.UNCHAINED_COSMOS_HTTP_URL || 'https://api.cosmos.shapeshift.com', - VITE_THORCHAIN_NODE_URL: process.env.THORCHAIN_NODE_URL || 'https://thornode.ninerealms.com', - VITE_MAYACHAIN_NODE_URL: process.env.MAYACHAIN_NODE_URL || 'https://tendermint.mayachain.info', - VITE_TRON_NODE_URL: process.env.TRON_NODE_URL || 'https://api.trongrid.io', - VITE_FEATURE_THORCHAINSWAP_LONGTAIL: process.env.FEATURE_THORCHAINSWAP_LONGTAIL === 'true', - VITE_FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL: - process.env.FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL === 'true', - VITE_THORCHAIN_MIDGARD_URL: - process.env.THORCHAIN_MIDGARD_URL || 'https://midgard.ninerealms.com/v2', - VITE_MAYACHAIN_MIDGARD_URL: - process.env.MAYACHAIN_MIDGARD_URL || 'https://midgard.mayachain.info/v2', - VITE_UNCHAINED_BITCOIN_HTTP_URL: - process.env.UNCHAINED_BITCOIN_HTTP_URL || 'https://api.bitcoin.shapeshift.com', - VITE_UNCHAINED_DOGECOIN_HTTP_URL: - process.env.UNCHAINED_DOGECOIN_HTTP_URL || 'https://api.dogecoin.shapeshift.com', - VITE_UNCHAINED_LITECOIN_HTTP_URL: - process.env.UNCHAINED_LITECOIN_HTTP_URL || 'https://api.litecoin.shapeshift.com', - VITE_UNCHAINED_BITCOINCASH_HTTP_URL: - process.env.UNCHAINED_BITCOINCASH_HTTP_URL || 'https://api.bitcoincash.shapeshift.com', - VITE_UNCHAINED_ETHEREUM_HTTP_URL: - process.env.UNCHAINED_ETHEREUM_HTTP_URL || 'https://api.ethereum.shapeshift.com', - VITE_UNCHAINED_AVALANCHE_HTTP_URL: - process.env.UNCHAINED_AVALANCHE_HTTP_URL || 'https://api.avalanche.shapeshift.com', - VITE_UNCHAINED_BNBSMARTCHAIN_HTTP_URL: - process.env.UNCHAINED_BNBSMARTCHAIN_HTTP_URL || 'https://api.bnbsmartchain.shapeshift.com', - VITE_UNCHAINED_BASE_HTTP_URL: - process.env.UNCHAINED_BASE_HTTP_URL || 'https://api.base.shapeshift.com', - VITE_COWSWAP_BASE_URL: process.env.COWSWAP_BASE_URL || 'https://api.cow.fi', - VITE_PORTALS_BASE_URL: process.env.PORTALS_BASE_URL || 'https://api.portals.fi', - VITE_ZRX_BASE_URL: process.env.ZRX_BASE_URL || 'https://api.proxy.shapeshift.com/api/v1/zrx/', - VITE_CHAINFLIP_API_KEY: process.env.CHAINFLIP_API_KEY || '', - VITE_CHAINFLIP_API_URL: process.env.CHAINFLIP_API_URL || 'https://chainflip-broker.io', - VITE_FEATURE_CHAINFLIP_SWAP_DCA: process.env.FEATURE_CHAINFLIP_SWAP_DCA === 'true', - VITE_JUPITER_API_URL: process.env.JUPITER_API_URL || 'https://quote-api.jup.ag/v6', - VITE_RELAY_API_URL: process.env.RELAY_API_URL || 'https://api.relay.link', - VITE_BEBOP_API_KEY: process.env.BEBOP_API_KEY || '', - VITE_NEAR_INTENTS_API_KEY: process.env.NEAR_INTENTS_API_KEY || '', - VITE_TENDERLY_API_KEY: process.env.TENDERLY_API_KEY || '', - VITE_TENDERLY_ACCOUNT_SLUG: process.env.TENDERLY_ACCOUNT_SLUG || '', - VITE_TENDERLY_PROJECT_SLUG: process.env.TENDERLY_PROJECT_SLUG || '', - VITE_SUI_NODE_URL: process.env.SUI_NODE_URL || 'https://fullnode.mainnet.sui.io', - VITE_ACROSS_API_URL: process.env.ACROSS_API_URL || 'https://app.across.to/api', - VITE_ACROSS_INTEGRATOR_ID: process.env.ACROSS_INTEGRATOR_ID || '', - VITE_DEBRIDGE_API_URL: process.env.DEBRIDGE_API_URL || 'https://dln.debridge.finance/v1.0', + VITE_UNCHAINED_THORCHAIN_HTTP_URL: env.UNCHAINED_THORCHAIN_HTTP_URL, + VITE_UNCHAINED_MAYACHAIN_HTTP_URL: env.UNCHAINED_MAYACHAIN_HTTP_URL, + VITE_UNCHAINED_COSMOS_HTTP_URL: env.UNCHAINED_COSMOS_HTTP_URL, + VITE_THORCHAIN_NODE_URL: env.THORCHAIN_NODE_URL, + VITE_MAYACHAIN_NODE_URL: env.MAYACHAIN_NODE_URL, + VITE_TRON_NODE_URL: env.TRON_NODE_URL, + VITE_FEATURE_THORCHAINSWAP_LONGTAIL: env.FEATURE_THORCHAINSWAP_LONGTAIL, + VITE_FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL: env.FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL, + VITE_THORCHAIN_MIDGARD_URL: env.THORCHAIN_MIDGARD_URL, + VITE_MAYACHAIN_MIDGARD_URL: env.MAYACHAIN_MIDGARD_URL, + VITE_UNCHAINED_BITCOIN_HTTP_URL: env.UNCHAINED_BITCOIN_HTTP_URL, + VITE_UNCHAINED_DOGECOIN_HTTP_URL: env.UNCHAINED_DOGECOIN_HTTP_URL, + VITE_UNCHAINED_LITECOIN_HTTP_URL: env.UNCHAINED_LITECOIN_HTTP_URL, + VITE_UNCHAINED_BITCOINCASH_HTTP_URL: env.UNCHAINED_BITCOINCASH_HTTP_URL, + VITE_UNCHAINED_ETHEREUM_HTTP_URL: env.UNCHAINED_ETHEREUM_HTTP_URL, + VITE_UNCHAINED_AVALANCHE_HTTP_URL: env.UNCHAINED_AVALANCHE_HTTP_URL, + VITE_UNCHAINED_BNBSMARTCHAIN_HTTP_URL: env.UNCHAINED_BNBSMARTCHAIN_HTTP_URL, + VITE_UNCHAINED_BASE_HTTP_URL: env.UNCHAINED_BASE_HTTP_URL, + VITE_COWSWAP_BASE_URL: env.COWSWAP_BASE_URL, + VITE_PORTALS_BASE_URL: env.PORTALS_BASE_URL, + VITE_ZRX_BASE_URL: env.ZRX_BASE_URL, + VITE_CHAINFLIP_API_KEY: env.CHAINFLIP_API_KEY, + VITE_CHAINFLIP_API_URL: env.CHAINFLIP_API_URL, + VITE_FEATURE_CHAINFLIP_SWAP_DCA: env.FEATURE_CHAINFLIP_SWAP_DCA, + VITE_JUPITER_API_URL: env.JUPITER_API_URL, + VITE_RELAY_API_URL: env.RELAY_API_URL, + VITE_BEBOP_API_KEY: env.BEBOP_API_KEY, + VITE_NEAR_INTENTS_API_KEY: env.NEAR_INTENTS_API_KEY, + VITE_TENDERLY_API_KEY: env.TENDERLY_API_KEY, + VITE_TENDERLY_ACCOUNT_SLUG: env.TENDERLY_ACCOUNT_SLUG, + VITE_TENDERLY_PROJECT_SLUG: env.TENDERLY_PROJECT_SLUG, + VITE_SUI_NODE_URL: env.SUI_NODE_URL, + VITE_ACROSS_API_URL: env.ACROSS_API_URL, + VITE_ACROSS_INTEGRATOR_ID: env.ACROSS_INTEGRATOR_ID, + VITE_DEBRIDGE_API_URL: env.DEBRIDGE_API_URL, }) - -// Swap service backend URL -const getSwapServiceBaseUrl = (): string => { - if (process.env.SWAP_SERVICE_BASE_URL) return process.env.SWAP_SERVICE_BASE_URL - if (process.env.NODE_ENV === 'production') { - throw new Error('SWAP_SERVICE_BASE_URL must be set in production') - } - console.warn('[config] SWAP_SERVICE_BASE_URL not set, using dev default') - return 'https://dev-api.swap-service.shapeshift.com' -} - -export const SWAP_SERVICE_BASE_URL = getSwapServiceBaseUrl() diff --git a/packages/public-api/src/env.ts b/packages/public-api/src/env.ts new file mode 100644 index 00000000000..011c967f178 --- /dev/null +++ b/packages/public-api/src/env.ts @@ -0,0 +1,88 @@ +import { z } from 'zod' + +const url = z.string().url() +const flag = z.string().transform(v => v === 'true') + +const envSchema = z.object({ + // Server + PORT: z.string().default('3005'), + NODE_ENV: z.string().default('development'), + + // Swap service + SWAP_SERVICE_BASE_URL: url, + + // Unchained URLs + UNCHAINED_ETHEREUM_HTTP_URL: url, + UNCHAINED_BITCOIN_HTTP_URL: url, + UNCHAINED_THORCHAIN_HTTP_URL: url, + UNCHAINED_MAYACHAIN_HTTP_URL: url, + UNCHAINED_COSMOS_HTTP_URL: url, + UNCHAINED_AVALANCHE_HTTP_URL: url, + UNCHAINED_BNBSMARTCHAIN_HTTP_URL: url, + UNCHAINED_BASE_HTTP_URL: url, + UNCHAINED_ARBITRUM_HTTP_URL: url, + UNCHAINED_OPTIMISM_HTTP_URL: url, + UNCHAINED_POLYGON_HTTP_URL: url, + UNCHAINED_GNOSIS_HTTP_URL: url, + UNCHAINED_DOGECOIN_HTTP_URL: url, + UNCHAINED_LITECOIN_HTTP_URL: url, + UNCHAINED_BITCOINCASH_HTTP_URL: url, + + // Node URLs + THORCHAIN_NODE_URL: url, + MAYACHAIN_NODE_URL: url, + TRON_NODE_URL: url, + SUI_NODE_URL: url, + + // Midgard URLs + THORCHAIN_MIDGARD_URL: url, + MAYACHAIN_MIDGARD_URL: url, + + // Swapper API URLs + COWSWAP_BASE_URL: url, + PORTALS_BASE_URL: url, + ZRX_BASE_URL: url, + JUPITER_API_URL: url, + RELAY_API_URL: url, + ACROSS_API_URL: url, + DEBRIDGE_API_URL: url, + CHAINFLIP_API_URL: url, + + // Swapper API keys (optional) + CHAINFLIP_API_KEY: z.string().default(''), + BEBOP_API_KEY: z.string().default(''), + NEAR_INTENTS_API_KEY: z.string().default(''), + TENDERLY_API_KEY: z.string().default(''), + TENDERLY_ACCOUNT_SLUG: z.string().default(''), + TENDERLY_PROJECT_SLUG: z.string().default(''), + ACROSS_INTEGRATOR_ID: z.string().default(''), + + // Feature flags + FEATURE_THORCHAINSWAP_LONGTAIL: flag, + FEATURE_THORCHAINSWAP_L1_TO_LONGTAIL: flag, + FEATURE_CHAINFLIP_SWAP_DCA: flag, + + // Affiliate + DEFAULT_AFFILIATE_BPS: z.string(), + + // Rate limiting + RATE_LIMIT_GLOBAL_MAX: z.coerce.number(), + RATE_LIMIT_DATA_MAX: z.coerce.number(), + RATE_LIMIT_SWAP_RATES_MAX: z.coerce.number(), + RATE_LIMIT_SWAP_QUOTE_MAX: z.coerce.number(), + RATE_LIMIT_SWAP_STATUS_MAX: z.coerce.number(), + RATE_LIMIT_AFFILIATE_STATS_MAX: z.coerce.number(), + RATE_LIMIT_AFFILIATE_MUTATION_MAX: z.coerce.number(), +}) + +const result = envSchema.safeParse(process.env) + +if (!result.success) { + console.error('Missing or invalid environment variables:') + for (const issue of result.error.issues) { + console.error(` ${issue.path.join('.')}: ${issue.message}`) + } + process.exit(1) +} + +export const env = result.data diff --git a/packages/public-api/src/index.ts b/packages/public-api/src/index.ts index d9abd622280..7e20298dc5b 100644 --- a/packages/public-api/src/index.ts +++ b/packages/public-api/src/index.ts @@ -4,6 +4,7 @@ import cors from 'cors' import express from 'express' import { getAssetsById, initAssets } from './assets' +import { env } from './env' import { quoteStore } from './lib/quoteStore' import { resolvePartnerCode } from './middleware/auth' import { @@ -22,8 +23,6 @@ import { getRates } from './routes/rates' import { getSwapStatus } from './routes/status' import { initSwapperDeps } from './swapperDeps' -const PORT = process.env.PORT || '3001' - const startServer = async () => { await initAssets() initSwapperDeps(getAssetsById()) @@ -75,8 +74,8 @@ const startServer = async () => { }, ) - app.listen(PORT, () => { - console.log(`Public API server running on port: ${PORT}`) + app.listen(env.PORT, () => { + console.log(`Public API server running on port: ${env.PORT}`) }) } diff --git a/packages/public-api/src/middleware/auth.ts b/packages/public-api/src/middleware/auth.ts index c2905ae98b3..e5ffe8e5c40 100644 --- a/packages/public-api/src/middleware/auth.ts +++ b/packages/public-api/src/middleware/auth.ts @@ -1,15 +1,12 @@ import type { NextFunction, Request, Response } from 'express' -const DEFAULT_AFFILIATE_BPS = '60' - -// Microservices URL for partner code resolution -const MICROSERVICES_URL = process.env.MICROSERVICES_URL || 'http://localhost:3001' +import { env } from '../env' const resolvePartnerCodeFromService = async ( code: string, ): Promise<{ affiliateAddress: string; bps: string } | null> => { try { - const response = await fetch(`${MICROSERVICES_URL}/v1/partner/${encodeURIComponent(code)}`) + const response = await fetch(`${env.SWAP_SERVICE_BASE_URL}/v1/partner/${encodeURIComponent(code)}`) if (response.ok) { const data = (await response.json()) as { @@ -51,7 +48,7 @@ export const resolvePartnerCode = async ( // No partner code provided — use default BPS for unattributed swaps req.affiliateInfo = { - affiliateBps: DEFAULT_AFFILIATE_BPS, + affiliateBps: env.DEFAULT_AFFILIATE_BPS, } next() diff --git a/packages/public-api/src/middleware/rateLimit.ts b/packages/public-api/src/middleware/rateLimit.ts index 6b240552a32..db8a4ecd76e 100644 --- a/packages/public-api/src/middleware/rateLimit.ts +++ b/packages/public-api/src/middleware/rateLimit.ts @@ -1,19 +1,14 @@ import type { Options, RateLimitRequestHandler } from 'express-rate-limit' import rateLimit from 'express-rate-limit' +import { env } from '../env' + const WINDOW_MS = 60 * 1000 export enum RateLimitErrorCode { RateLimitExceeded = 'RATE_LIMIT_EXCEEDED', } -const parseEnvInt = (key: string, defaultValue: number): number => { - const value = process.env[key] - if (!value) return defaultValue - const parsed = parseInt(value, 10) - return isNaN(parsed) ? defaultValue : parsed -} - const rateLimitHandler: Options['handler'] = (_req, res) => { res.status(429).json({ error: 'Too many requests, please try again later', @@ -21,18 +16,19 @@ const rateLimitHandler: Options['handler'] = (_req, res) => { }) } -const createLimiter = (envKey: string, defaultMax: number): RateLimitRequestHandler => +const createLimiter = (max: number): RateLimitRequestHandler => rateLimit({ windowMs: WINDOW_MS, - max: parseEnvInt(envKey, defaultMax), + max, standardHeaders: 'draft-7', legacyHeaders: false, handler: rateLimitHandler, }) -export const globalLimiter = createLimiter('RATE_LIMIT_GLOBAL_MAX', 300) -export const dataLimiter = createLimiter('RATE_LIMIT_DATA_MAX', 120) -export const swapRatesLimiter = createLimiter('RATE_LIMIT_SWAP_RATES_MAX', 60) -export const swapQuoteLimiter = createLimiter('RATE_LIMIT_SWAP_QUOTE_MAX', 45) -export const swapStatusLimiter = createLimiter('RATE_LIMIT_SWAP_STATUS_MAX', 60) -export const affiliateStatsLimiter = createLimiter('RATE_LIMIT_AFFILIATE_STATS_MAX', 30) +export const globalLimiter = createLimiter(env.RATE_LIMIT_GLOBAL_MAX) +export const dataLimiter = createLimiter(env.RATE_LIMIT_DATA_MAX) +export const swapRatesLimiter = createLimiter(env.RATE_LIMIT_SWAP_RATES_MAX) +export const swapQuoteLimiter = createLimiter(env.RATE_LIMIT_SWAP_QUOTE_MAX) +export const swapStatusLimiter = createLimiter(env.RATE_LIMIT_SWAP_STATUS_MAX) +export const affiliateStatsLimiter = createLimiter(env.RATE_LIMIT_AFFILIATE_STATS_MAX) +export const affiliateMutationLimiter = createLimiter(env.RATE_LIMIT_AFFILIATE_MUTATION_MAX) diff --git a/packages/public-api/src/routes/affiliate/getAffiliateStats.ts b/packages/public-api/src/routes/affiliate/getAffiliateStats.ts index cce4096f9cd..eb88db2c6ad 100644 --- a/packages/public-api/src/routes/affiliate/getAffiliateStats.ts +++ b/packages/public-api/src/routes/affiliate/getAffiliateStats.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express' -import { SWAP_SERVICE_BASE_URL } from '../../config' +import { env } from '../../env' import { registry } from '../../registry' import type { ErrorResponse } from '../../types' import type { AffiliateStatsResponse } from './types' @@ -59,7 +59,7 @@ export const getAffiliateStats = async (req: Request, res: Response): Promise const getController = new AbortController() const getTimeout = setTimeout(() => getController.abort(), STATUS_TIMEOUT_MS) try { - const swapResponse = await fetch(`${SWAP_SERVICE_BASE_URL}/swaps/${quoteId}`, { + const swapResponse = await fetch(`${env.SWAP_SERVICE_BASE_URL}/swaps/${quoteId}`, { signal: getController.signal, }) if (swapResponse.ok) { diff --git a/packages/public-api/src/routes/status/utils.ts b/packages/public-api/src/routes/status/utils.ts index 831d5919628..0839b042536 100644 --- a/packages/public-api/src/routes/status/utils.ts +++ b/packages/public-api/src/routes/status/utils.ts @@ -1,5 +1,5 @@ import { getAsset } from '../../assets' -import { SWAP_SERVICE_BASE_URL } from '../../config' +import { env } from '../../env' import type { quoteStore } from '../../lib/quoteStore' import { STATUS_TIMEOUT_MS } from './constants' @@ -53,7 +53,7 @@ export const registerSwapInService = async ( const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), STATUS_TIMEOUT_MS) try { - const postResponse = await fetch(`${SWAP_SERVICE_BASE_URL}/swaps`, { + const postResponse = await fetch(`${env.SWAP_SERVICE_BASE_URL}/swaps`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller.signal, diff --git a/packages/public-api/src/swapperDeps.ts b/packages/public-api/src/swapperDeps.ts index 1930b453d48..ef2830cbce1 100644 --- a/packages/public-api/src/swapperDeps.ts +++ b/packages/public-api/src/swapperDeps.ts @@ -16,6 +16,7 @@ import type { AssetsByIdPartial } from '@shapeshiftoss/types' import { KnownChainIds } from '@shapeshiftoss/types' import { getServerConfig } from './config' +import { env } from './env' type GasFeeData = { gasPrice: string @@ -35,33 +36,23 @@ type UtxoNetworkFees = { slow: { satsPerKiloByte: number } } -const getEvmUnchainedUrls = (): Record => { - const config = getServerConfig() - return { - [KnownChainIds.EthereumMainnet]: config.VITE_UNCHAINED_ETHEREUM_HTTP_URL, - [KnownChainIds.ArbitrumMainnet]: - process.env.UNCHAINED_ARBITRUM_HTTP_URL || 'https://api.arbitrum.shapeshift.com', - [KnownChainIds.OptimismMainnet]: - process.env.UNCHAINED_OPTIMISM_HTTP_URL || 'https://api.optimism.shapeshift.com', - [KnownChainIds.PolygonMainnet]: - process.env.UNCHAINED_POLYGON_HTTP_URL || 'https://api.polygon.shapeshift.com', - [KnownChainIds.GnosisMainnet]: - process.env.UNCHAINED_GNOSIS_HTTP_URL || 'https://api.gnosis.shapeshift.com', - [KnownChainIds.AvalancheMainnet]: config.VITE_UNCHAINED_AVALANCHE_HTTP_URL, - [KnownChainIds.BnbSmartChainMainnet]: config.VITE_UNCHAINED_BNBSMARTCHAIN_HTTP_URL, - [KnownChainIds.BaseMainnet]: config.VITE_UNCHAINED_BASE_HTTP_URL, - } -} +const getEvmUnchainedUrls = (): Record => ({ + [KnownChainIds.EthereumMainnet]: env.UNCHAINED_ETHEREUM_HTTP_URL, + [KnownChainIds.ArbitrumMainnet]: env.UNCHAINED_ARBITRUM_HTTP_URL, + [KnownChainIds.OptimismMainnet]: env.UNCHAINED_OPTIMISM_HTTP_URL, + [KnownChainIds.PolygonMainnet]: env.UNCHAINED_POLYGON_HTTP_URL, + [KnownChainIds.GnosisMainnet]: env.UNCHAINED_GNOSIS_HTTP_URL, + [KnownChainIds.AvalancheMainnet]: env.UNCHAINED_AVALANCHE_HTTP_URL, + [KnownChainIds.BnbSmartChainMainnet]: env.UNCHAINED_BNBSMARTCHAIN_HTTP_URL, + [KnownChainIds.BaseMainnet]: env.UNCHAINED_BASE_HTTP_URL, +}) -const getUtxoUnchainedUrls = (): Record => { - const config = getServerConfig() - return { - [KnownChainIds.BitcoinMainnet]: config.VITE_UNCHAINED_BITCOIN_HTTP_URL, - [KnownChainIds.DogecoinMainnet]: config.VITE_UNCHAINED_DOGECOIN_HTTP_URL, - [KnownChainIds.LitecoinMainnet]: config.VITE_UNCHAINED_LITECOIN_HTTP_URL, - [KnownChainIds.BitcoinCashMainnet]: config.VITE_UNCHAINED_BITCOINCASH_HTTP_URL, - } -} +const getUtxoUnchainedUrls = (): Record => ({ + [KnownChainIds.BitcoinMainnet]: env.UNCHAINED_BITCOIN_HTTP_URL, + [KnownChainIds.DogecoinMainnet]: env.UNCHAINED_DOGECOIN_HTTP_URL, + [KnownChainIds.LitecoinMainnet]: env.UNCHAINED_LITECOIN_HTTP_URL, + [KnownChainIds.BitcoinCashMainnet]: env.UNCHAINED_BITCOINCASH_HTTP_URL, +}) const GAS_FEES_TIMEOUT_MS = 10_000 From 07374a16b6f0e5512a4402f3c24a353f5b14a585 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:12:24 -0600 Subject: [PATCH 4/6] lint fix --- packages/public-api/src/middleware/auth.ts | 4 +++- packages/public-api/src/routes/status/getSwapStatus.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/public-api/src/middleware/auth.ts b/packages/public-api/src/middleware/auth.ts index e5ffe8e5c40..33acd837c01 100644 --- a/packages/public-api/src/middleware/auth.ts +++ b/packages/public-api/src/middleware/auth.ts @@ -6,7 +6,9 @@ const resolvePartnerCodeFromService = async ( code: string, ): Promise<{ affiliateAddress: string; bps: string } | null> => { try { - const response = await fetch(`${env.SWAP_SERVICE_BASE_URL}/v1/partner/${encodeURIComponent(code)}`) + const response = await fetch( + `${env.SWAP_SERVICE_BASE_URL}/v1/partner/${encodeURIComponent(code)}`, + ) if (response.ok) { const data = (await response.json()) as { diff --git a/packages/public-api/src/routes/status/getSwapStatus.ts b/packages/public-api/src/routes/status/getSwapStatus.ts index 5720aae8d69..8965daefdb3 100644 --- a/packages/public-api/src/routes/status/getSwapStatus.ts +++ b/packages/public-api/src/routes/status/getSwapStatus.ts @@ -1,7 +1,7 @@ import type { Request, Response } from 'express' -import { quoteStore } from '../../lib/quoteStore' import { env } from '../../env' +import { quoteStore } from '../../lib/quoteStore' import { registry } from '../../registry' import type { ErrorResponse } from '../../types' import { PartnerCodeHeaderSchema } from '../../types' From 039468346ac238080d82f99c281587781507e513 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:18:13 -0600 Subject: [PATCH 5/6] improve url construction --- packages/public-api/src/routes/affiliate/getAffiliateStats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/public-api/src/routes/affiliate/getAffiliateStats.ts b/packages/public-api/src/routes/affiliate/getAffiliateStats.ts index eb88db2c6ad..35728bd777b 100644 --- a/packages/public-api/src/routes/affiliate/getAffiliateStats.ts +++ b/packages/public-api/src/routes/affiliate/getAffiliateStats.ts @@ -59,7 +59,7 @@ export const getAffiliateStats = async (req: Request, res: Response): Promise Date: Tue, 24 Mar 2026 12:19:35 -0600 Subject: [PATCH 6/6] improve zod validation --- packages/public-api/src/env.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/public-api/src/env.ts b/packages/public-api/src/env.ts index 011c967f178..d265fa903f1 100644 --- a/packages/public-api/src/env.ts +++ b/packages/public-api/src/env.ts @@ -1,11 +1,11 @@ import { z } from 'zod' const url = z.string().url() -const flag = z.string().transform(v => v === 'true') +const flag = z.enum(['true', 'false']).transform(v => v === 'true') const envSchema = z.object({ // Server - PORT: z.string().default('3005'), + PORT: z.string().regex(/^\d+$/, 'PORT must be numeric').default('3005'), NODE_ENV: z.string().default('development'), // Swap service @@ -63,16 +63,16 @@ const envSchema = z.object({ FEATURE_CHAINFLIP_SWAP_DCA: flag, // Affiliate - DEFAULT_AFFILIATE_BPS: z.string(), + DEFAULT_AFFILIATE_BPS: z.string().regex(/^\d+$/, 'DEFAULT_AFFILIATE_BPS must be numeric'), // Rate limiting - RATE_LIMIT_GLOBAL_MAX: z.coerce.number(), - RATE_LIMIT_DATA_MAX: z.coerce.number(), - RATE_LIMIT_SWAP_RATES_MAX: z.coerce.number(), - RATE_LIMIT_SWAP_QUOTE_MAX: z.coerce.number(), - RATE_LIMIT_SWAP_STATUS_MAX: z.coerce.number(), - RATE_LIMIT_AFFILIATE_STATS_MAX: z.coerce.number(), - RATE_LIMIT_AFFILIATE_MUTATION_MAX: z.coerce.number(), + RATE_LIMIT_GLOBAL_MAX: z.coerce.number().int().min(1), + RATE_LIMIT_DATA_MAX: z.coerce.number().int().min(1), + RATE_LIMIT_SWAP_RATES_MAX: z.coerce.number().int().min(1), + RATE_LIMIT_SWAP_QUOTE_MAX: z.coerce.number().int().min(1), + RATE_LIMIT_SWAP_STATUS_MAX: z.coerce.number().int().min(1), + RATE_LIMIT_AFFILIATE_STATS_MAX: z.coerce.number().int().min(1), + RATE_LIMIT_AFFILIATE_MUTATION_MAX: z.coerce.number().int().min(1), }) const result = envSchema.safeParse(process.env)