Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
867ec03
feat: add public swap API server
0xApotheosis Jan 7, 2026
0486644
fix(public-api): skip postinstall scripts in Docker build
0xApotheosis Jan 7, 2026
70d9292
fix(public-api): use --mode skip-build for yarn install in Docker
0xApotheosis Jan 7, 2026
d1d4079
fix(public-api): fix TypeScript compilation errors
0xApotheosis Jan 7, 2026
9d069f3
fix(public-api): add Java to Docker for unchained-client code generation
0xApotheosis Jan 7, 2026
fd87c74
fix(unchained-client): add shebang and executable permission to post_…
0xApotheosis Jan 7, 2026
c36cc4e
fix(public-api): copy node_modules to runner stage
0xApotheosis Jan 7, 2026
3646070
feat(public-api): add Scalar docs with auto-expanded sections and pre…
0xApotheosis Jan 7, 2026
6f82a01
fix(public-api): downgrade zod-to-openapi to v7 for zod v3 compatibility
0xApotheosis Jan 7, 2026
e3ab006
perf(public-api): remove node_modules from production Docker image
0xApotheosis Jan 7, 2026
e30bb00
docs(public-api): add integration overview to API documentation
0xApotheosis Jan 7, 2026
fd54d0d
perf(public-api): add BuildKit optimizations to Docker build
0xApotheosis Jan 7, 2026
664ce12
fix(public-api): add required id to BuildKit cache mount
0xApotheosis Jan 7, 2026
e1df430
fix(public-api): remove BuildKit cache mount
0xApotheosis Jan 7, 2026
1098997
perf(public-api): add Railway-compatible BuildKit cache mount
0xApotheosis Jan 7, 2026
0904d80
chore: add .playwright-mcp to gitignore
0xApotheosis Jan 7, 2026
53e2616
fix(public-api): bundle cowprotocol packages to fix production rates
0xApotheosis Jan 7, 2026
e337e9d
docs(public-api): use 0x swapper in quote examples
0xApotheosis Jan 7, 2026
a08e34d
fix(public-api): set page title to ShapeShift API Reference
0xApotheosis Jan 7, 2026
7a8ada2
fix(public-api): use valid example addresses in quote schema
0xApotheosis Jan 7, 2026
1d1ff0c
fix(public-api): resolve consistent-type-imports lint errors
0xApotheosis Jan 7, 2026
d815e63
feat(public-api): add API info JSON at root endpoint
0xApotheosis Jan 7, 2026
d0c29a4
fix: lint
0xApotheosis Jan 7, 2026
2043894
fix(public-api): return Promise.resolve() for early return in initAssets
0xApotheosis Jan 7, 2026
e41ebc3
fix(public-api): handle decimal values in buyAmountCryptoBaseUnit sort
0xApotheosis Jan 7, 2026
64018e8
fix(utils): use lodash-es for ESM compatibility
0xApotheosis Jan 7, 2026
f777a78
fix(public-api): handle undefined array access in buyAmountCryptoBase…
0xApotheosis Jan 7, 2026
edcee4f
fix(swapper): use direct @mysten/sui import in CetusSwapper
0xApotheosis Jan 7, 2026
bca990c
feat(public-api): add smoke test suite for Railway deployment
0xApotheosis Jan 8, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ yarn-error.log*
# idea
*.iml
.idea/
.playwright-mcp/
21 changes: 21 additions & 0 deletions packages/public-api/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Server Configuration
PORT=3001
HOST=0.0.0.0
NODE_ENV=production

# 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

# 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
77 changes: 77 additions & 0 deletions packages/public-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# syntax=docker/dockerfile:1

# Build stage
FROM node:22-alpine AS builder

WORKDIR /app

# Install build dependencies (including Java for unchained-client code generation)
RUN apk add --no-cache python3 make g++ openjdk17-jre

# Copy workspace files
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn ./.yarn

# Copy all package.json files for workspace resolution
COPY packages/public-api/package.json ./packages/public-api/
COPY packages/caip/package.json ./packages/caip/
COPY packages/types/package.json ./packages/types/
COPY packages/utils/package.json ./packages/utils/
COPY packages/swapper/package.json ./packages/swapper/
COPY packages/chain-adapters/package.json ./packages/chain-adapters/
COPY packages/unchained-client/package.json ./packages/unchained-client/
COPY packages/contracts/package.json ./packages/contracts/
COPY packages/errors/package.json ./packages/errors/

# Install dependencies with Railway cache mount (skip build/postinstall scripts that require git/cypress/etc)
RUN --mount=type=cache,id=s/5fa4b228-de48-4bb3-90d6-807d0c6e7f1c-/root/.yarn/berry/cache,target=/root/.yarn/berry/cache \
yarn install --immutable --mode skip-build

# Copy unchained-client config and generator files FIRST (rarely changes, enables layer caching)
COPY packages/unchained-client/openapitools.json ./packages/unchained-client/
COPY packages/unchained-client/generator/ ./packages/unchained-client/generator/

# Generate unchained-client code (cached when openapitools.json unchanged)
# Cache mount for OpenAPI Generator JAR to avoid re-downloading on each build
RUN --mount=type=cache,id=s/5fa4b228-de48-4bb3-90d6-807d0c6e7f1c-/root/.openapitools,target=/root/.openapitools \
yarn workspace @shapeshiftoss/unchained-client generate

# Copy remaining source files
COPY packages/ ./packages/
COPY public/generated/generatedAssetData.json ./public/generated/
COPY tsconfig.json tsconfig.packages.json ./

# Build all packages using parallel tsc with project references
RUN yarn run -T tsc --build tsconfig.packages.json && \
yarn workspaces foreach -ptiv --include '@shapeshiftoss/{errors,types,contracts,caip,utils,unchained-client,chain-adapters,swapper,public-api}' run postbuild

# Build the public-api bundle
WORKDIR /app/packages/public-api
RUN yarn node esbuild.config.mjs

# Build smoke tests bundle
RUN yarn node esbuild.smoke-tests.mjs

# Production stage
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/packages/public-api/dist/smoke-tests.cjs ./smoke-tests.cjs
COPY --from=builder /app/public/generated/generatedAssetData.json ./public/generated/

# Set environment
ENV NODE_ENV=production
ENV ASSET_DATA_PATH=/app/public/generated/generatedAssetData.json
ENV PORT=3001
ENV HOST=0.0.0.0

EXPOSE 3001

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1

CMD ["node", "server.cjs"]
30 changes: 30 additions & 0 deletions packages/public-api/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Dependencies (yarn install handles these fresh)
node_modules/
packages/*/node_modules/

# Git
.git/

# Build artifacts
build/
packages/*/dist/

# Development files
.env*
!.env.example
*.log
.DS_Store
coverage/

# IDE
.idea/
.vscode/

# Test files not needed for build
cypress/
**/*.test.ts
**/*.test.tsx
**/*.spec.ts

# Other large/unnecessary directories
.playwright-mcp/
25 changes: 25 additions & 0 deletions packages/public-api/esbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as esbuild from 'esbuild'

const result = await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/server.cjs',
platform: 'node',
target: 'node20',
format: 'cjs',
sourcemap: true,
external: [
// Keep native modules external
'fsevents',
],
alias: {
// Alias ethers/lib/utils to ethers which has the utils re-exported
'ethers/lib/utils': 'ethers',
},
define: {
'process.env.NODE_ENV': '"production"',
},
logLevel: 'info',
})

console.log('Build complete:', result)
15 changes: 15 additions & 0 deletions packages/public-api/esbuild.smoke-tests.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as esbuild from 'esbuild'

const result = await esbuild.build({
entryPoints: ['tests/run-smoke-tests.ts'],
bundle: true,
outfile: 'dist/smoke-tests.cjs',
platform: 'node',
target: 'node20',
format: 'cjs',
sourcemap: true,
external: ['fsevents'],
logLevel: 'info',
})

console.log('Smoke tests build complete:', result)
46 changes: 46 additions & 0 deletions packages/public-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@shapeshiftoss/public-api",
"version": "0.1.0",
"packageManager": "[email protected]",
"repository": "https://github.com/shapeshift/web",
"license": "MIT",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "yarn clean && yarn run -T tsc --build",
"build:bundle": "node esbuild.config.mjs",
"build:smoke-tests": "node esbuild.smoke-tests.mjs",
"clean": "rm -rf dist",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js",
"start:prod": "node dist/server.cjs",
"test:smoke": "tsx tests/run-smoke-tests.ts",
"docker:build": "docker build -t shapeshift-public-api -f Dockerfile ../../",
"docker:run": "docker run -p 3001:3001 --env-file .env shapeshift-public-api"
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.4",
"@scalar/express-api-reference": "^0.8.30",
"@shapeshiftoss/caip": "workspace:^",
"@shapeshiftoss/chain-adapters": "workspace:^",
"@shapeshiftoss/swapper": "workspace:^",
"@shapeshiftoss/types": "workspace:^",
"@shapeshiftoss/utils": "workspace:^",
"@sniptt/monads": "^0.5.10",
"@types/swagger-ui-express": "^4.1.8",
"cors": "^2.8.5",
"express": "^4.21.0",
"uuid": "^9.0.0",
"yaml": "^2.8.2",
"zod": "3.23.8"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^22.10.4",
"@types/uuid": "^9.0.5",
"tsx": "^4.19.2",
"typescript": "^5.7.3"
}
}
21 changes: 21 additions & 0 deletions packages/public-api/railway.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[build]
builder = "dockerfile"
dockerfilePath = "packages/public-api/Dockerfile"
watchPatterns = [
"packages/public-api/**",
"packages/swapper/**",
"packages/caip/**",
"packages/types/**",
"packages/utils/**",
"packages/unchained-client/**",
"packages/chain-adapters/**",
"packages/contracts/**",
"packages/errors/**",
"public/generated/generatedAssetData.json"
]

[deploy]
healthcheckPath = "/health"
healthcheckTimeout = 30
restartPolicyType = "on_failure"
restartPolicyMaxRetries = 5
75 changes: 75 additions & 0 deletions packages/public-api/src/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { AssetId } from '@shapeshiftoss/caip'
import type { Asset, AssetsById } from '@shapeshiftoss/types'
import { getBaseAsset } from '@shapeshiftoss/utils'
import fs from 'fs'
import path from 'path'

let assetsById: AssetsById = {}
let assetIds: AssetId[] = []
let assets: Asset[] = []
let initialized = false

export const initAssets = (): Promise<void> => {
if (initialized) return Promise.resolve()

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) {
console.warn('Asset data file not found in any of the expected locations:', possiblePaths)
initialized = true
return Promise.resolve()
}

const assetDataJson = JSON.parse(fs.readFileSync(assetDataPath, 'utf8'))
const localAssetData = assetDataJson.byId
const sortedAssetIds = assetDataJson.ids as AssetId[]

// Enrich assets with chain-level data
const enrichedAssetsById: AssetsById = {}
for (const assetId of sortedAssetIds) {
const asset = localAssetData[assetId]
if (asset) {
const baseAsset = getBaseAsset(asset.chainId)
enrichedAssetsById[assetId] = {
...asset,
networkName: baseAsset?.networkName,
explorer: baseAsset?.explorer,
explorerAddressLink: baseAsset?.explorerAddressLink,
explorerTxLink: baseAsset?.explorerTxLink,
}
}
}

assetsById = enrichedAssetsById
assetIds = sortedAssetIds
assets = sortedAssetIds.map((id: AssetId) => enrichedAssetsById[id]).filter(Boolean) as Asset[]

console.log(`Loaded ${assetIds.length} assets from ${assetDataPath}`)
} catch (error) {
console.error('Failed to load assets:', error)
}

initialized = true
return Promise.resolve()
}

export const getAssetsById = (): AssetsById => assetsById
export const getAssetIds = (): AssetId[] => assetIds
export const getAllAssets = (): Asset[] => assets
export const getAsset = (assetId: AssetId): Asset | undefined => assetsById[assetId]
63 changes: 63 additions & 0 deletions packages/public-api/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { SwapperConfig } from '@shapeshiftoss/swapper'

// Server-side config that mirrors the web app's config but from environment variables
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.thorchain.info/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',
})

// Default affiliate fee in basis points
export const DEFAULT_AFFILIATE_BPS = '55'

// API server config
export const API_PORT = parseInt(process.env.PORT || '3001', 10)
export const API_HOST = process.env.HOST || '0.0.0.0'

// Static API keys for testing (in production these would come from a database)
export const STATIC_API_KEYS: Record<string, { name: string; feeSharePercentage: number }> = {
'test-api-key-123': { name: 'Test Partner', feeSharePercentage: 50 },
}
Loading