Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
9508ba6
feat: add public swap API server
0xApotheosis Jan 7, 2026
b27a1a5
fix(public-api): skip postinstall scripts in Docker build
0xApotheosis Jan 7, 2026
2e59395
fix(public-api): use --mode skip-build for yarn install in Docker
0xApotheosis Jan 7, 2026
16d5d73
fix(public-api): fix TypeScript compilation errors
0xApotheosis Jan 7, 2026
43af3d1
fix(public-api): add Java to Docker for unchained-client code generation
0xApotheosis Jan 7, 2026
0882a56
fix(unchained-client): add shebang and executable permission to post_…
0xApotheosis Jan 7, 2026
28059a7
fix(public-api): copy node_modules to runner stage
0xApotheosis Jan 7, 2026
0e4ac6f
feat(public-api): add Scalar docs with auto-expanded sections and pre…
0xApotheosis Jan 7, 2026
ad3e656
fix(public-api): downgrade zod-to-openapi to v7 for zod v3 compatibility
0xApotheosis Jan 7, 2026
5a7deb0
perf(public-api): remove node_modules from production Docker image
0xApotheosis Jan 7, 2026
c7bd50c
docs(public-api): add integration overview to API documentation
0xApotheosis Jan 7, 2026
d402052
perf(public-api): add BuildKit optimizations to Docker build
0xApotheosis Jan 7, 2026
1525286
fix(public-api): add required id to BuildKit cache mount
0xApotheosis Jan 7, 2026
99e3fe2
fix(public-api): remove BuildKit cache mount
0xApotheosis Jan 7, 2026
c61df6a
perf(public-api): add Railway-compatible BuildKit cache mount
0xApotheosis Jan 7, 2026
18ad697
chore: add .playwright-mcp to gitignore
0xApotheosis Jan 7, 2026
56b5c7f
fix(public-api): bundle cowprotocol packages to fix production rates
0xApotheosis Jan 7, 2026
fdd0f0d
docs(public-api): use 0x swapper in quote examples
0xApotheosis Jan 7, 2026
b0f1bc6
fix(public-api): set page title to ShapeShift API Reference
0xApotheosis Jan 7, 2026
fa90b20
fix(public-api): use valid example addresses in quote schema
0xApotheosis Jan 7, 2026
2c95f0c
fix(public-api): resolve consistent-type-imports lint errors
0xApotheosis Jan 7, 2026
bcecc74
feat(public-api): add API info JSON at root endpoint
0xApotheosis Jan 7, 2026
3a0ca7f
fix: lint
0xApotheosis Jan 7, 2026
d41406e
fix(public-api): return Promise.resolve() for early return in initAssets
0xApotheosis Jan 7, 2026
cd4014c
fix(public-api): handle decimal values in buyAmountCryptoBaseUnit sort
0xApotheosis Jan 7, 2026
83778ce
fix(utils): use lodash-es for ESM compatibility
0xApotheosis Jan 7, 2026
88047fa
fix(public-api): handle undefined array access in buyAmountCryptoBase…
0xApotheosis Jan 7, 2026
611026f
fix(swapper): use direct @mysten/sui import in CetusSwapper
0xApotheosis Jan 7, 2026
87bef29
feat(public-api): add smoke test suite for Railway deployment
0xApotheosis Jan 8, 2026
522202f
fix(railway): move swap-widget config to package level
0xApotheosis Jan 19, 2026
8c68847
fix(public-api): include all workspace packages in Dockerfile
0xApotheosis Jan 19, 2026
63a8c98
fix(public-api): add missing assertGetTonChainAdapter to SwapperDeps
0xApotheosis Jan 19, 2026
1c9fd63
fix(public-api): only set initialized flag after successful asset load
0xApotheosis Jan 19, 2026
a0fd0c6
fix(public-api): return 400 for malformed URL encoding in getAssetById
0xApotheosis Jan 19, 2026
4036742
fix(public-api): use isNativeEvmAsset for robust native asset detection
0xApotheosis Jan 19, 2026
3b144ea
fix(public-api): add missing affiliateBps and networkFeeCryptoBaseUni…
0xApotheosis Jan 19, 2026
9a8cf16
chore(public-api): lint fixes
0xApotheosis Jan 19, 2026
85f904d
fix(public-api): coerce boolean and number query params from strings
0xApotheosis Jan 19, 2026
ffadcc3
fix(public-api): use env vars for EVM unchained URLs instead of hardc…
0xApotheosis Jan 19, 2026
688304a
docs(public-api): add CLAUDE.md with LLM instructions
0xApotheosis Jan 19, 2026
4d0f528
docs: update quote endpoint example to swap ETH to BTC
0xApotheosis Jan 19, 2026
3e1527a
fix(public-api): implement UTXO chain adapter for BTC sell support
0xApotheosis Jan 19, 2026
27f5e1d
fix(swapper): handle unsupported chains gracefully in Portals and Cha…
0xApotheosis Jan 19, 2026
029d3bd
fix(public-api): add strict regex validation to rates request schema
0xApotheosis Jan 20, 2026
3545771
fix(public-api): replace unsafe any-cast with typed TradeQuoteError f…
0xApotheosis Jan 20, 2026
318e59c
fix(public-api): add timeout to fetchGasFees using AbortController
0xApotheosis Jan 20, 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/
Comment thread
0xApotheosis marked this conversation as resolved.
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
Comment on lines +19 to +21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure it's a reliable way, we will need to add a way to generate API keys automatically or a sort of dashboard to ease the integration

If we have a polished product, we could run a marketing campaign and see if it hooks

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure, we'll definitely have an actual dashboard for this soon - this is just MVP stuff!

15 changes: 15 additions & 0 deletions packages/public-api/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Public API - LLM Instructions

## Running the Server Locally (Recommended)

Use bundled mode to avoid ESM/tsx module resolution issues:

```bash
yarn build:bundle && yarn start:prod
```

This bundles everything with esbuild into `dist/server.cjs` and runs it with plain Node.js.

## Code Style

This codebase does **not** use semicolons. Follow the existing style in each file.
78 changes: 78 additions & 0 deletions packages/public-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 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 (yarn requires all workspaces to be present)
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/
COPY packages/swap-widget/package.json ./packages/swap-widget/

# 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": "yarn@3.5.0",
"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
77 changes: 77 additions & 0 deletions packages/public-api/src/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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'),
Comment on lines +20 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: what happens if the project isn't rebuilt because not caught up by the railway stuff when we merge asset data PRs? Are we getting out of data assets?

We can probably fix that by fetching the generatedAssetData.json as a XHR to app.shapeshift.com then cache it, so we ensure we always use the latest upstream dataset

].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 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}`)
initialized = true
return Promise.resolve()
} catch (error) {
console.error('Failed to load assets:', error)
return Promise.reject(error)
}
}

export const getAssetsById = (): AssetsById => assetsById
export const getAssetIds = (): AssetId[] => assetIds
export const getAllAssets = (): Asset[] => assets
export const getAsset = (assetId: AssetId): Asset | undefined => assetsById[assetId]
Loading