feat: add public swap API server - #11586
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a new packages/public-api Express service with assets, rates, quote, docs endpoints, API-key auth, OpenAPI generation, server-side swapper dependency plumbing, build tooling (esbuild, Docker), smoke tests, types, and several monorepo and swapper package adjustments. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Router as "API Router"
participant Auth as "Auth Middleware"
participant Assets as "Asset Service"
participant Swapper as "Swapper Module"
participant External as "External Services"
Client->>Router: GET /v1/swap/rates + X-API-Key
Router->>Auth: validate X-API-Key
Auth-->>Router: PartnerConfig
Router->>Assets: getAsset(sellAssetId), getAsset(buyAssetId)
Assets-->>Router: asset data
Router->>Swapper: lazy-load swapper, request rates
Swapper->>External: fetch rates / gas / chain data
External-->>Swapper: responses or errors
Swapper-->>Router: aggregated per-swapper rates
Router-->>Client: RatesResponse (sorted, errors annotated)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
bca990c to
84e88b9
Compare
Implements a public API for swap quotes and rates that reuses the existing @shapeshiftoss/swapper package without code duplication. Endpoints: - GET /v1/swap/rates - Get rates from all swappers - POST /v1/swap/quote - Get executable quote with tx data - GET /v1/assets - List supported assets Features: - API key authentication middleware - Minimal EVM chain adapters for gas fee estimation - esbuild bundling for single-file deployment - Docker + Railway deployment configuration - 55 bps affiliate fee (planned 50/50 split with partners) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace YARN_ENABLE_SCRIPTS=false with --mode skip-build to properly skip all lifecycle scripts including postinstall that requires git - Add tsconfig files copy for TypeScript compilation - Add build:docker script to unchained-client to skip Java-based code generation - Use explicit workspace builds instead of yarn workspaces foreach
- Type JSON response in swapperDeps.ts to fix 'data is of type unknown' error - Add GetTradeRateInput type assertion in rates.ts for chainId compatibility - Add GetTradeQuoteInputWithWallet type assertion in quote.ts for chainId compatibility
- Install openjdk17-jre in build stage for OpenAPI generator CLI - Use full unchained-client build script instead of build:docker
…process.sh Fixes 'Exec format error' during Docker build.
Fixes runtime module not found errors for external dependencies like @cowprotocol/app-data
…filled examples - Add Scalar API reference UI at /docs endpoint - Configure defaultOpenAllTags to auto-expand all sections on page load - Prefill test API key (test-api-key-123) in authentication section - Add OpenAPI example values to all request schemas for prefilled forms - Set up zod-to-openapi for generating OpenAPI spec from Zod schemas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
zod-to-openapi v8.x requires zod v4 as a peer dependency, causing runtime errors when used with zod v3.23.8. Downgrade to v7.3.4 which supports zod v3. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
esbuild bundles all dependencies into server.cjs, so copying the entire node_modules directory to the production image was unnecessary and added significant time to Railway deployments (~2-4 min) and image size (~500MB+). The only externals are fsevents (macOS-only, not used in Linux containers) and @cowprotocol/* (not used by public-api routes). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive description explaining how to integrate the swap API: - Step-by-step integration flow (assets -> rates -> quote -> execute) - Authentication requirements - CAIP-19 asset ID format with examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Dockerfile.dockerignore to exclude node_modules (4.5GB), .git (1.4GB), and other unnecessary files from build context - Enable BuildKit with syntax directive for advanced features - Add cache mount for yarn berry cache to speed up subsequent builds These optimizations reduce build context transfer time and cache dependencies between builds, further improving Railway deployment speed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Railway requires cache mounts to have an explicit id parameter. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Railway's cache mount requires a specific prefix format that's not documented. Reverting to standard yarn install - the .dockerignore still provides significant build context reduction. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Query parameters are always received as strings in Express. The Zod schemas for rates and quote endpoints expected actual boolean/number types, causing validation errors when allowMultiHop was passed as "true" or "false" string from the docs example. - Create shared booleanFromString utility to preprocess string booleans - Update rates.ts to use booleanFromString for allowMultiHop - Update quote.ts to use booleanFromString and z.coerce.number() Co-Authored-By: Claude <noreply@anthropic.com>
…oded values Allow staging/multi-tenant environments to override EVM chain endpoints via environment variables while preserving existing defaults as fallbacks. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@packages/public-api/src/routes/rates.ts`:
- Around line 136-151: Replace the unsafe any-cast fallback for error.code in
the error object: instead of error.code || ('UnknownError' as any), use nullish
coalescing to provide the properly-typed fallback (e.g., error.code ??
TradeQuoteError.UnknownError or a typed 'UnknownError' constant). Update the
construction in the block that handles result.isErr() (where you call
result.unwrapErr() and return the error object alongside swapperName, rate,
etc.) so the fallback value is correctly typed and no longer casts to any.
- Around line 12-20: The RatesRequestSchema currently allows any non-empty
string for sellAmountCryptoBaseUnit and slippageTolerancePercentageDecimal; add
strict regex validation to these fields so malformed numeric input is rejected
at the API boundary: update RatesRequestSchema to replace
sellAmountCryptoBaseUnit: z.string().min(1) with a regex that only permits a
positive integer (e.g., /^\d+$/) and replace slippageTolerancePercentageDecimal:
z.string().optional() with a regex that permits a non-negative decimal number
(e.g., /^(?:\d+)(?:\.\d+)?$/) while preserving optional/default behavior; use
z.string().regex(...) (or z.string().refine(...)) so Zod returns a 400 on
invalid formats and leave allowMultiHop and other fields unchanged.
In `@packages/public-api/src/swapperDeps.ts`:
- Around line 52-75: The fetchGasFees function currently uses a raw fetch
without a timeout; replace it with the project's timeout pattern by invoking the
existing fetchWithTimeout (or implement the same AbortController pattern) when
calling `${unchainedUrl}/api/v1/gas/fees` inside fetchGasFees so the request is
aborted on timeout; align the timeout duration with other swapper operations
(e.g., the 60s used by timeoutMonadic or choose a context-appropriate value) and
preserve the same error handling behavior (throwing an Error when response.ok is
false) while removing any comment/claim that fetch isn't available in Node 22.
🧹 Nitpick comments (2)
packages/public-api/src/swapperDeps.ts (1)
1-30: Align gas-fee types with chain-adapter exports and add explicit helper return types.Local GasFeeData/GasFeeDataEstimate can drift from chain-adapters’ canonical types; also the helper functions lack explicit return types. Consider importing the shared types (if exported) and annotating helper returns to keep the contract tight. As per coding guidelines, use explicit function return types.
♻️ Suggested refactor
-import type { - ChainAdapter, - CosmosSdkChainAdapter, - EvmChainAdapter, - near, - solana, - starknet, - sui, - ton, - tron, - UtxoChainAdapter, -} from '@shapeshiftoss/chain-adapters' +import type { + ChainAdapter, + CosmosSdkChainAdapter, + EvmChainAdapter, + GasFeeData, + GasFeeDataEstimate, + near, + solana, + starknet, + sui, + ton, + tron, + UtxoChainAdapter, +} from '@shapeshiftoss/chain-adapters' @@ -type GasFeeData = { - gasPrice: string - maxFeePerGas?: string - maxPriorityFeePerGas?: string -} - -type GasFeeDataEstimate = { - fast: GasFeeData - average: GasFeeData - slow: GasFeeData -} +type MinimalEvmChainAdapter = Pick< + EvmChainAdapter, + 'getChainId' | 'getGasFeeData' | 'getFeeAssetId' | 'getDisplayName' +> @@ -const createMinimalEvmAdapter = (chainId: ChainId) => { +const createMinimalEvmAdapter = (chainId: ChainId): MinimalEvmChainAdapter => { @@ -const createStubAdapter = (type: string) => { +const createStubAdapter = (type: string): ((chainId: ChainId) => never) => {Also applies to: 77-122
packages/public-api/src/routes/rates.ts (1)
39-47: Add explicit return types for lazy-import helpers.These helpers currently rely on inference; add explicit return types to keep the module boundary tight. As per coding guidelines, use explicit function return types.
♻️ Suggested typing
-let swapperModule: Awaited<ReturnType<typeof importSwapperModule>> | null = null -const importSwapperModule = () => import('@shapeshiftoss/swapper') -const getSwapperModule = async () => { +let swapperModule: Awaited<ReturnType<typeof importSwapperModule>> | null = null +const importSwapperModule = (): Promise<typeof import('@shapeshiftoss/swapper')> => + import('@shapeshiftoss/swapper') +const getSwapperModule = async (): Promise<typeof import('@shapeshiftoss/swapper')> => { if (!swapperModule) { swapperModule = await importSwapperModule() } return swapperModule }
Change the sample request in the OpenAPI docs from ETH→USDC to ETH→BTC using the Relay swapper, with appropriate Bitcoin receive address. Co-Authored-By: Claude <noreply@anthropic.com>
NeOMakinG
left a comment
There was a problem hiding this comment.
I've been consuming the stuff since a few days already, really happy to get that in for the sake of progression
Gave it a bit of a smoke test in https://jam.dev/c/a8071036-ee77-4dfb-b2a0-9c185963846d even thought it's missing a lot of parts (BTC, SOL, any other chains), we will be able to test it more as long as we add more chains support from the widget (there is an open PR for Solana and BTC)
| # 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
For sure, we'll definitely have an actual dashboard for this soon - this is just MVP stuff!
| path.join(process.cwd(), 'public/generated/generatedAssetData.json'), | ||
| path.join(process.cwd(), '../../public/generated/generatedAssetData.json'), | ||
| path.join(process.cwd(), 'generatedAssetData.json'), |
There was a problem hiding this comment.
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
|
|
||
| // 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 }, |
There was a problem hiding this comment.
In the future we will probably want fees to be configurable at quote time with our base fees on top of that
| // Asset endpoints (optional auth) | ||
| v1Router.get('/assets', optionalApiKeyAuth, getAssets) | ||
| v1Router.get('/assets/count', optionalApiKeyAuth, getAssetCount) | ||
| v1Router.get('/assets/:assetId(*)', optionalApiKeyAuth, getAssetById) |
There was a problem hiding this comment.
We will probably want to add some cache on top of that at some point
There was a problem hiding this comment.
We will want to provide a way for our users to check the market price of any asset
| const ENABLED_SWAPPER_NAMES = [ | ||
| 'THORChain', | ||
| 'MAYAChain', | ||
| '0x', | ||
| 'CoW Swap', | ||
| 'Portals', | ||
| 'Chainflip', | ||
| 'Jupiter', | ||
| 'Relay', | ||
| 'ButterSwap', | ||
| 'Bebop', |
There was a problem hiding this comment.
Heyyyy, we need to make it better! It's missing a few swappers there, is this something we are planning to bring soon?
You can remove Jupiter already if you want, it's not working anymore and won't be working as supported by relay under the hood
| const createStubAdapter = (type: string) => { | ||
| return () => { | ||
| throw new Error( | ||
| `Chain adapter ${type} not implemented in public API. ` + | ||
| `This swapper requires chain adapter functionality that is not yet available.`, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
What, in the end we will 100% be able to use the chain adapters directly, providing more accurate fee estimation etc, but do we even need them?
THORChain BTC sell trades were failing because assertGetUtxoChainAdapter was a stub that threw an error. This implements a minimal UTXO adapter that provides the required interface for rate requests. Co-Authored-By: Claude <noreply@anthropic.com>
…inflip - Update default THORChain Midgard URL to ninerealms.com for reliability - Fix Portals swapper to validate chain support before calling EVM adapter - Fix Chainflip swapper to handle undefined error properties with optional chaining Co-Authored-By: Claude <noreply@anthropic.com>
Validate sellAmountCryptoBaseUnit with /^\d+$/ to only accept positive integers and slippageTolerancePercentageDecimal with /^(?:\d+)(?:\.\d+)?$/ to accept non-negative decimals, rejecting malformed numeric input at the API boundary. Co-Authored-By: Claude <noreply@anthropic.com>
…allback Use nullish coalescing with TradeQuoteError.UnknownError instead of casting 'UnknownError' as any for proper type safety. Co-Authored-By: Claude <noreply@anthropic.com>
Prevents hanging requests when unchained gas fees endpoint is slow/unresponsive. Uses 10s timeout consistent with RATE_TIMEOUT_MS in rates route. Co-Authored-By: Claude <noreply@anthropic.com>
|
Thanks @NeOMakinG, I'll merge this guy and tackle your comments in a follow-up PR. |
Description
Adds a new
@shapeshiftoss/public-apipackage that exposes swap functionality as a REST API for external partners.Production URL: https://api.shapeshift.com
API Documentation: https://api.shapeshift.com/docs
Endpoints:
GET /health- Health checkGET /v1/assets- List all supported assetsGET /v1/swap/rates- Fetch rates from all enabled swappers (0x, Relay, THORChain, etc.)GET /v1/swap/quote- Get executable quote with full transaction dataGET /docs- Interactive Scalar API documentationArchitecture:
@shapeshiftoss/swapperdirectly - zero code duplicationSwapperDepswith minimal EVM adapters fetching gas fees from UnchainedNote: Affiliate fee sharing is not yet implemented. Currently all swaps use the standard DAO affiliate fee (55 bps). Partner attribution, fee tracking, and revenue sharing will be added in Phase 2.
Railway Config Changes:
railway.jsonintopackages/swap-widget/railway.tomlrailway.jsonwhich was overriding the public-api package configIssue (if applicable)
closes #
Risk
Low-Medium - This is a new standalone package/service that doesn't modify any existing swap flows in the web app.
None directly. The public API reads from existing swapper infrastructure but doesn't introduce new on-chain transactions.
Testing
Engineering
Operations
This is a new standalone service deployed to Railway. It does not affect the main web application.
Screenshots (if applicable)
N/A - API service, no UI changes.
Summary by CodeRabbit
New Features
Infrastructure
Chores
✏️ Tip: You can customize this high-level summary in your review settings.