-
Notifications
You must be signed in to change notification settings - Fork 199
feat: add public swap API server #11586
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9508ba6
b27a1a5
2e59395
16d5d73
43af3d1
0882a56
28059a7
0e4ac6f
ad3e656
5a7deb0
c7bd50c
d402052
1525286
99e3fe2
c61df6a
18ad697
56b5c7f
fdd0f0d
b0f1bc6
fa90b20
2c95f0c
bcecc74
3a0ca7f
d41406e
cd4014c
83778ce
88047fa
611026f
87bef29
522202f
8c68847
63a8c98
1c9fd63
a0fd0c6
4036742
3b144ea
9a8cf16
85f904d
ffadcc3
688304a
4d0f528
3e1527a
27f5e1d
029d3bd
3545771
318e59c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,3 +42,4 @@ yarn-error.log* | |
| # idea | ||
| *.iml | ||
| .idea/ | ||
| .playwright-mcp/ | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! |
||
| 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. |
| 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"] |
| 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/ |
| 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) |
| 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) |
| 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" | ||
| } | ||
| } |
| 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 |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| ].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] | ||
Uh oh!
There was an error while loading. Please reload this page.