Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 67 additions & 26 deletions packages/public-api/.env.example
Original file line number Diff line number Diff line change
@@ -1,28 +1,69 @@
# Server Configuration
PORT=3001
HOST=0.0.0.0
PORT=3005
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=
# 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_<PARTNER_ID>=<api_key>:<name>:<fee_share_percentage>
# 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
10 changes: 5 additions & 5 deletions packages/public-api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
25 changes: 1 addition & 24 deletions packages/public-api/src/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,7 @@ export const initAssets = (): Promise<void> => {
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
Expand Down
103 changes: 37 additions & 66 deletions packages/public-api/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,70 +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()

// API server config
export const API_PORT = parseInt(process.env.PORT || '3001', 10)
export const API_HOST = process.env.HOST || '0.0.0.0'
88 changes: 88 additions & 0 deletions packages/public-api/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { z } from 'zod'

const url = z.string().url()
const flag = z.enum(['true', 'false']).transform(v => v === 'true')

const envSchema = z.object({
// Server
PORT: z.string().regex(/^\d+$/, 'PORT must be numeric').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().regex(/^\d+$/, 'DEFAULT_AFFILIATE_BPS must be numeric'),

// Rate limiting
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)

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
8 changes: 4 additions & 4 deletions packages/public-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cors from 'cors'
import express from 'express'

import { getAssetsById, initAssets } from './assets'
import { API_HOST, API_PORT } from './config'
import { env } from './env'
import { quoteStore } from './lib/quoteStore'
import { resolvePartnerCode } from './middleware/auth'
import {
Expand All @@ -29,7 +29,7 @@ const startServer = async () => {

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())
Expand Down Expand Up @@ -74,8 +74,8 @@ const startServer = async () => {
},
)

app.listen(API_PORT, API_HOST, () => {
console.log(`Public API server running at http://${API_HOST}:${API_PORT}`)
app.listen(env.PORT, () => {
console.log(`Public API server running on port: ${env.PORT}`)
})
}

Expand Down
Loading
Loading