diff --git a/.gitignore b/.gitignore index 8692757aefa..6be6cb15e17 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ yarn-error.log* # idea *.iml .idea/ +.playwright-mcp/ diff --git a/packages/public-api/.env.example b/packages/public-api/.env.example new file mode 100644 index 00000000000..d800eaf6015 --- /dev/null +++ b/packages/public-api/.env.example @@ -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_=:: +# Example: API_KEY_PARTNER1=abc123:MyPartner:50 diff --git a/packages/public-api/Dockerfile b/packages/public-api/Dockerfile new file mode 100644 index 00000000000..fab29091929 --- /dev/null +++ b/packages/public-api/Dockerfile @@ -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"] diff --git a/packages/public-api/Dockerfile.dockerignore b/packages/public-api/Dockerfile.dockerignore new file mode 100644 index 00000000000..9614c140cf7 --- /dev/null +++ b/packages/public-api/Dockerfile.dockerignore @@ -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/ diff --git a/packages/public-api/esbuild.config.mjs b/packages/public-api/esbuild.config.mjs new file mode 100644 index 00000000000..368664106aa --- /dev/null +++ b/packages/public-api/esbuild.config.mjs @@ -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) diff --git a/packages/public-api/esbuild.smoke-tests.mjs b/packages/public-api/esbuild.smoke-tests.mjs new file mode 100644 index 00000000000..5426e7227c1 --- /dev/null +++ b/packages/public-api/esbuild.smoke-tests.mjs @@ -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) diff --git a/packages/public-api/package.json b/packages/public-api/package.json new file mode 100644 index 00000000000..45fc380ea63 --- /dev/null +++ b/packages/public-api/package.json @@ -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" + } +} diff --git a/packages/public-api/railway.toml b/packages/public-api/railway.toml new file mode 100644 index 00000000000..4badc5a3e70 --- /dev/null +++ b/packages/public-api/railway.toml @@ -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 diff --git a/packages/public-api/src/assets.ts b/packages/public-api/src/assets.ts new file mode 100644 index 00000000000..3460e982713 --- /dev/null +++ b/packages/public-api/src/assets.ts @@ -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 => { + 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] diff --git a/packages/public-api/src/config.ts b/packages/public-api/src/config.ts new file mode 100644 index 00000000000..44610242c08 --- /dev/null +++ b/packages/public-api/src/config.ts @@ -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 = { + 'test-api-key-123': { name: 'Test Partner', feeSharePercentage: 50 }, +} diff --git a/packages/public-api/src/docs/openapi.ts b/packages/public-api/src/docs/openapi.ts new file mode 100644 index 00000000000..12f95da6d51 --- /dev/null +++ b/packages/public-api/src/docs/openapi.ts @@ -0,0 +1,277 @@ +import '../setupZod' + +import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi' +import { z } from 'zod' + +import { AssetRequestSchema, AssetsListRequestSchema } from '../routes/assets' +import { QuoteRequestSchema } from '../routes/quote' +import { RatesRequestSchema } from '../routes/rates' + +export const registry = new OpenAPIRegistry() + +// Register reusable schemas +// We should probably define the response schemas with Zod too, but for now we'll do best effort with the request schemas +// and basic response structures. + +// Security Schemes +registry.registerComponent('securitySchemes', 'apiKeyAuth', { + type: 'apiKey', + in: 'header', + name: 'X-API-Key', +}) + +// --- Definitions --- + +// Asset +const AssetSchema = registry.register( + 'Asset', + z.object({ + assetId: z.string().openapi({ example: 'eip155:1/slip44:60' }), + chainId: z.string().openapi({ example: 'eip155:1' }), + name: z.string().openapi({ example: 'Ethereum' }), + symbol: z.string().openapi({ example: 'ETH' }), + precision: z.number().openapi({ example: 18 }), + color: z.string().openapi({ example: '#5C6BC0' }), + icon: z.string().openapi({ example: 'https://assets.coincap.io/assets/icons/eth@2x.png' }), + explorer: z.string().openapi({ example: 'https://etherscan.io' }), + explorerAddressLink: z.string().openapi({ example: 'https://etherscan.io/address/' }), + explorerTxLink: z.string().openapi({ example: 'https://etherscan.io/tx/' }), + }), +) + +// Quote Response Step +const QuoteStepSchema = registry.register( + 'QuoteStep', + z.object({ + sellAsset: AssetSchema, + buyAsset: AssetSchema, + sellAmountCryptoBaseUnit: z.string().openapi({ example: '1000000000000000000' }), + buyAmountAfterFeesCryptoBaseUnit: z.string().openapi({ example: '995000000' }), + allowanceContract: z + .string() + .openapi({ example: '0xdef1c0ded9bec7f1a1670819833240f027b25eff' }), + estimatedExecutionTimeMs: z.number().optional().openapi({ example: 60000 }), + source: z.string().openapi({ example: '0x' }), + transactionData: z + .object({ + to: z.string(), + data: z.string(), + value: z.string(), + gasLimit: z.string().optional(), + }) + .optional(), + }), +) + +// Quote Response +const QuoteResponseSchema = registry.register( + 'QuoteResponse', + z.object({ + quoteId: z.string().uuid(), + swapperName: z.string().openapi({ example: '0x' }), + rate: z.string().openapi({ example: '0.995' }), + sellAsset: AssetSchema, + buyAsset: AssetSchema, + sellAmountCryptoBaseUnit: z.string(), + buyAmountBeforeFeesCryptoBaseUnit: z.string(), + buyAmountAfterFeesCryptoBaseUnit: z.string(), + affiliateBps: z.string().openapi({ example: '10' }), + slippageTolerancePercentageDecimal: z.string().optional().openapi({ example: '0.01' }), + steps: z.array(QuoteStepSchema), + expiresAt: z.number(), + }), +) + +// Rate Response +const RateResponseSchema = registry.register( + 'RateResponse', + z.object({ + rates: z.array( + z.object({ + swapperName: z.string(), + rate: z.string(), + buyAmountCryptoBaseUnit: z.string(), + sellAmountCryptoBaseUnit: z.string(), + steps: z.number(), + estimatedExecutionTimeMs: z.number().optional(), + priceImpactPercentageDecimal: z.string().optional(), + error: z + .object({ + code: z.string(), + message: z.string(), + }) + .optional(), + }), + ), + timestamp: z.number(), + expiresAt: z.number(), + }), +) + +// --- Paths --- + +// GET /v1/assets +registry.registerPath({ + method: 'get', + path: '/v1/assets', + summary: 'List supported assets', + description: 'Get a list of all supported assets, optionally filtered by chain.', + tags: ['Assets'], + request: { + query: AssetsListRequestSchema, + }, + responses: { + 200: { + description: 'List of assets', + content: { + 'application/json': { + schema: z.object({ + assets: z.array(AssetSchema), + timestamp: z.number(), + }), + }, + }, + }, + }, +}) + +// GET /v1/assets/{assetId} +registry.registerPath({ + method: 'get', + path: '/v1/assets/{assetId}', + summary: 'Get asset by ID', + description: 'Get details of a specific asset by its ID (URL encoded).', + tags: ['Assets'], + request: { + params: AssetRequestSchema, + }, + responses: { + 200: { + description: 'Asset details', + content: { + 'application/json': { + schema: AssetSchema, + }, + }, + }, + 404: { + description: 'Asset not found', + }, + }, +}) + +// GET /v1/swap/rates +registry.registerPath({ + method: 'get', + path: '/v1/swap/rates', + summary: 'Get swap rates', + description: + 'Get informative swap rates from all available swappers. This does not create a transaction.', + tags: ['Swaps'], + security: [{ apiKeyAuth: [] }], + request: { + query: RatesRequestSchema, + }, + responses: { + 200: { + description: 'Swap rates', + content: { + 'application/json': { + schema: RateResponseSchema, + }, + }, + }, + 400: { + description: 'Invalid request', + }, + }, +}) + +// POST /v1/swap/quote +registry.registerPath({ + method: 'post', + path: '/v1/swap/quote', + summary: 'Get executable quote', + description: + 'Get an executable quote for a swap, including transaction data. Requires a specific swapper name.', + tags: ['Swaps'], + security: [{ apiKeyAuth: [] }], + request: { + body: { + content: { + 'application/json': { + schema: QuoteRequestSchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'Swap quote', + content: { + 'application/json': { + schema: QuoteResponseSchema, + }, + }, + }, + 400: { + description: 'Invalid request or unavailable swapper', + }, + }, +}) + +export const generateOpenApiDocument = () => { + const generator = new OpenApiGeneratorV3(registry.definitions) + + return generator.generateDocument({ + openapi: '3.0.0', + info: { + version: '1.0.0', + title: 'ShapeShift Public API', + description: `The ShapeShift Public API enables developers to integrate multi-chain swap functionality into their applications. Access rates from multiple DEX aggregators and execute swaps across supported blockchains. + +## Integration Overview + +### 1. Get Supported Assets +First, fetch the list of supported assets to populate your UI: +\`\`\` +GET /v1/assets +\`\`\` + +### 2. Get Swap Rates +When a user wants to swap, fetch rates from all available swappers to find the best deal: +\`\`\` +GET /v1/swap/rates?sellAssetId=eip155:1/slip44:60&buyAssetId=eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&sellAmountCryptoBaseUnit=1000000000000000000 +\`\`\` +This returns rates from THORChain, 0x, CoW Swap, and other supported swappers. + +### 3. Get Executable Quote +Once the user selects a rate, request an executable quote with transaction data: +\`\`\` +POST /v1/swap/quote +{ + "sellAssetId": "eip155:1/slip44:60", + "buyAssetId": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "sellAmountCryptoBaseUnit": "1000000000000000000", + "swapperName": "0x", + "receiveAddress": "0x...", + "sendAddress": "0x..." +} +\`\`\` + +### 4. Execute the Swap +Use the returned \`transactionData\` to build and sign a transaction with the user's wallet, then broadcast it to the network. + +## Authentication +Include your API key in the \`X-API-Key\` header for all swap endpoints. + +## Asset IDs +Assets use CAIP-19 format: \`{chainId}/{assetNamespace}:{assetReference}\` +- Native ETH: \`eip155:1/slip44:60\` +- USDC on Ethereum: \`eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\` +- Native BTC: \`bip122:000000000019d6689c085ae165831e93/slip44:0\` +`, + }, + servers: [{ url: 'https://api.shapeshift.com' }, { url: 'http://localhost:3001' }], + }) +} diff --git a/packages/public-api/src/index.ts b/packages/public-api/src/index.ts new file mode 100644 index 00000000000..efce70190c3 --- /dev/null +++ b/packages/public-api/src/index.ts @@ -0,0 +1,92 @@ +import './setupZod' + +import cors from 'cors' +import express from 'express' + +import { initAssets } from './assets' +import { API_HOST, API_PORT } from './config' +import { apiKeyAuth, optionalApiKeyAuth } from './middleware/auth' +import { getAssetById, getAssetCount, getAssets } from './routes/assets' +import { docsRouter } from './routes/docs' +import { getQuote } from './routes/quote' +import { getRates } from './routes/rates' + +const app = express() + +// Middleware +app.use(cors()) +app.use(express.json()) + +// Root endpoint - API info +app.get('/', (_req, res) => { + res.json({ + name: 'ShapeShift API', + version: '1.0.0', + description: 'Decentralized swap and asset discovery API', + documentation: '/docs', + endpoints: { + health: 'GET /health', + assets: 'GET /v1/assets', + assetCount: 'GET /v1/assets/count', + assetById: 'GET /v1/assets/:assetId', + swapRates: 'GET /v1/swap/rates', + swapQuote: 'POST /v1/swap/quote', + }, + }) +}) + +// Health check (no auth required) +app.get('/health', (_req, res) => { + res.json({ status: 'ok', timestamp: Date.now() }) +}) + +// API v1 routes +const v1Router = express.Router() + +// Swap endpoints (require API key) +v1Router.get('/swap/rates', apiKeyAuth, getRates) +v1Router.post('/swap/quote', apiKeyAuth, getQuote) + +// Asset endpoints (optional auth) +v1Router.get('/assets', optionalApiKeyAuth, getAssets) +v1Router.get('/assets/count', optionalApiKeyAuth, getAssetCount) +v1Router.get('/assets/:assetId(*)', optionalApiKeyAuth, getAssetById) + +app.use('/v1', v1Router) +app.use('/docs', docsRouter) + +// 404 handler +app.use((_req, res) => { + res.status(404).json({ error: 'Not found' }) +}) + +// Error handler +app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { + console.error('Unhandled error:', err) + res.status(500).json({ error: 'Internal server error' }) +}) + +// Start server +const startServer = async () => { + console.log('Initializing assets...') + await initAssets() + + app.listen(API_PORT, API_HOST, () => { + console.log(`Public API server running at http://${API_HOST}:${API_PORT}`) + console.log(` +Available endpoints: + GET /health - Health check + GET /v1/swap/rates - Get swap rates from all swappers + POST /v1/swap/quote - Get executable quote with tx data + GET /v1/assets - List supported assets + GET /v1/assets/count - Get asset count + GET /v1/assets/:assetId - Get single asset by ID + +Authentication: + Include 'X-API-Key' header with your API key for /v1/swap/* endpoints. + Test API key: test-api-key-123 + `) + }) +} + +startServer().catch(console.error) diff --git a/packages/public-api/src/middleware/auth.ts b/packages/public-api/src/middleware/auth.ts new file mode 100644 index 00000000000..c859cc5c66e --- /dev/null +++ b/packages/public-api/src/middleware/auth.ts @@ -0,0 +1,83 @@ +import crypto from 'crypto' +import type { NextFunction, Request, Response } from 'express' + +import { STATIC_API_KEYS } from '../config' +import type { ErrorResponse, PartnerConfig } from '../types' + +// Hash an API key for comparison (in production, keys would be stored hashed) +const hashApiKey = (key: string): string => { + return crypto.createHash('sha256').update(key).digest('hex') +} + +// API key authentication middleware +export const apiKeyAuth = (req: Request, res: Response, next: NextFunction): void => { + const apiKey = req.header('X-API-Key') + + if (!apiKey) { + const errorResponse: ErrorResponse = { + error: 'API key required', + code: 'MISSING_API_KEY', + } + res.status(401).json(errorResponse) + return + } + + // Look up the partner by API key + // In production, this would query a database + const partnerInfo = STATIC_API_KEYS[apiKey] + + if (!partnerInfo) { + const errorResponse: ErrorResponse = { + error: 'Invalid API key', + code: 'INVALID_API_KEY', + } + res.status(401).json(errorResponse) + return + } + + // Attach partner info to request + const partner: PartnerConfig = { + id: hashApiKey(apiKey).substring(0, 16), // Use hash prefix as ID + apiKeyHash: hashApiKey(apiKey), + name: partnerInfo.name, + feeSharePercentage: partnerInfo.feeSharePercentage, + status: 'active', + rateLimit: { + requestsPerMinute: 60, + requestsPerDay: 10000, + }, + createdAt: new Date(), + } + + req.partner = partner + + next() +} + +// Optional auth - allows unauthenticated requests but attaches partner info if present +export const optionalApiKeyAuth = (req: Request, _res: Response, next: NextFunction): void => { + const apiKey = req.header('X-API-Key') + + if (apiKey) { + const partnerInfo = STATIC_API_KEYS[apiKey] + + if (partnerInfo) { + const partner: PartnerConfig = { + id: hashApiKey(apiKey).substring(0, 16), + apiKeyHash: hashApiKey(apiKey), + name: partnerInfo.name, + feeSharePercentage: partnerInfo.feeSharePercentage, + status: 'active', + rateLimit: { + requestsPerMinute: 60, + requestsPerDay: 10000, + }, + createdAt: new Date(), + } + + req.partner = partner + } + } + + next() +} diff --git a/packages/public-api/src/routes/assets.ts b/packages/public-api/src/routes/assets.ts new file mode 100644 index 00000000000..83aea66732f --- /dev/null +++ b/packages/public-api/src/routes/assets.ts @@ -0,0 +1,92 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { getAllAssets, getAsset, getAssetIds } from '../assets' +import type { AssetsResponse, ErrorResponse } from '../types' + +// Request validation schema for single asset +export const AssetRequestSchema = z.object({ + assetId: z.string().min(1).openapi({ example: 'eip155:1/slip44:60' }), +}) + +// Request validation schema for filtered list +export const AssetsListRequestSchema = z.object({ + chainId: z.string().optional().openapi({ example: 'eip155:1' }), + limit: z.coerce.number().min(1).max(1000).optional().default(100).openapi({ example: 100 }), + offset: z.coerce.number().min(0).optional().default(0).openapi({ example: 0 }), +}) + +export const getAssets = (req: Request, res: Response): void => { + try { + const parseResult = AssetsListRequestSchema.safeParse(req.query) + if (!parseResult.success) { + const errorResponse: ErrorResponse = { + error: 'Invalid request parameters', + details: parseResult.error.errors, + } + res.status(400).json(errorResponse) + return + } + + const { chainId, limit, offset } = parseResult.data + + let assets = getAllAssets() + + // Filter by chain if specified + if (chainId) { + assets = assets.filter(asset => asset.chainId === chainId) + } + + // Apply pagination + const paginatedAssets = assets.slice(offset, offset + limit) + + const response: AssetsResponse = { + assets: paginatedAssets, + timestamp: Date.now(), + } + + res.json(response) + } catch (error) { + console.error('Error in getAssets:', error) + res.status(500).json({ error: 'Internal server error' } as ErrorResponse) + } +} + +export const getAssetById = (req: Request, res: Response): void => { + try { + const parseResult = AssetRequestSchema.safeParse(req.params) + if (!parseResult.success) { + const errorResponse: ErrorResponse = { + error: 'Invalid request parameters', + details: parseResult.error.errors, + } + res.status(400).json(errorResponse) + return + } + + const { assetId } = parseResult.data + // URL decode the assetId since it contains special characters + const decodedAssetId = decodeURIComponent(assetId) + const asset = getAsset(decodedAssetId) + + if (!asset) { + res.status(404).json({ error: `Asset not found: ${decodedAssetId}` } as ErrorResponse) + return + } + + res.json(asset) + } catch (error) { + console.error('Error in getAssetById:', error) + res.status(500).json({ error: 'Internal server error' } as ErrorResponse) + } +} + +export const getAssetCount = (_req: Request, res: Response): void => { + try { + const count = getAssetIds().length + res.json({ count, timestamp: Date.now() }) + } catch (error) { + console.error('Error in getAssetCount:', error) + res.status(500).json({ error: 'Internal server error' } as ErrorResponse) + } +} diff --git a/packages/public-api/src/routes/docs.ts b/packages/public-api/src/routes/docs.ts new file mode 100644 index 00000000000..0434728335f --- /dev/null +++ b/packages/public-api/src/routes/docs.ts @@ -0,0 +1,41 @@ +import { apiReference } from '@scalar/express-api-reference' +import express from 'express' + +import { generateOpenApiDocument } from '../docs/openapi' + +const router = express.Router() + +// Generate Spec +const openApiDocument = generateOpenApiDocument() + +// Serve raw JSON spec +router.get('/json', (_req, res) => { + res.json(openApiDocument) +}) + +// Serve Scalar UI +router.use( + '/', + apiReference({ + spec: { + content: openApiDocument, + }, + pageTitle: 'ShapeShift API Reference', + theme: 'purple', + showSidebar: true, + hideDownloadButton: true, + darkMode: true, + defaultOpenAllTags: true, + authentication: { + preferredSecurityScheme: 'apiKeyAuth', + apiKey: { + token: 'test-api-key-123', + }, + }, + customCss: ` + .sidebar { --theme-color-1: #383838; } + `, + } as any), +) + +export const docsRouter = router diff --git a/packages/public-api/src/routes/quote.ts b/packages/public-api/src/routes/quote.ts new file mode 100644 index 00000000000..8f255f59571 --- /dev/null +++ b/packages/public-api/src/routes/quote.ts @@ -0,0 +1,282 @@ +import { fromChainId } from '@shapeshiftoss/caip' +import type { + GetTradeQuoteInputWithWallet, + SwapperName, + TradeQuote, + TradeQuoteStep, +} from '@shapeshiftoss/swapper' +import type { Request, Response } from 'express' +import { v4 as uuidv4 } from 'uuid' +import { z } from 'zod' + +import { getAsset, getAssetsById } from '../assets' +import { DEFAULT_AFFILIATE_BPS } from '../config' +import { createServerSwapperDeps } from '../swapperDeps' +import type { ApiQuoteStep, ApprovalInfo, ErrorResponse, QuoteResponse } from '../types' + +// Lazy load swapper to avoid import issues at module load time +let swapperModule: Awaited> | null = null +const importSwapperModule = () => import('@shapeshiftoss/swapper') +const getSwapperModule = async () => { + if (!swapperModule) { + swapperModule = await importSwapperModule() + } + return swapperModule +} + +// Request validation schema - swapperName is string, validated later +export const QuoteRequestSchema = z.object({ + sellAssetId: z.string().min(1).openapi({ example: 'eip155:1/slip44:60' }), + buyAssetId: z + .string() + .min(1) + .openapi({ example: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' }), + sellAmountCryptoBaseUnit: z.string().min(1).openapi({ example: '1000000000000000000' }), + receiveAddress: z + .string() + .min(1) + .openapi({ example: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28' }), + sendAddress: z + .string() + .optional() + .openapi({ example: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28' }), + swapperName: z.string().min(1).openapi({ example: '0x' }), + slippageTolerancePercentageDecimal: z.string().optional().openapi({ example: '0.01' }), + allowMultiHop: z.boolean().optional().default(true).openapi({ example: true }), + accountNumber: z.number().optional().default(0).openapi({ example: 0 }), +}) + +// Helper to extract transaction data from quote step +const extractTransactionData = (step: TradeQuoteStep): ApiQuoteStep['transactionData'] => { + // Check for various swapper-specific transaction metadata + if (step.zrxTransactionMetadata) { + return { + to: step.zrxTransactionMetadata.to, + data: step.zrxTransactionMetadata.data, + value: step.zrxTransactionMetadata.value, + gasLimit: step.zrxTransactionMetadata.gas, + } + } + + if (step.portalsTransactionMetadata) { + return { + to: step.portalsTransactionMetadata.to, + data: step.portalsTransactionMetadata.data, + value: step.portalsTransactionMetadata.value, + gasLimit: step.portalsTransactionMetadata.gasLimit, + } + } + + if (step.bebopTransactionMetadata) { + return { + to: step.bebopTransactionMetadata.to, + data: step.bebopTransactionMetadata.data, + value: step.bebopTransactionMetadata.value, + gasLimit: step.bebopTransactionMetadata.gas, + } + } + + if (step.butterSwapTransactionMetadata) { + return { + to: step.butterSwapTransactionMetadata.to, + data: step.butterSwapTransactionMetadata.data, + value: step.butterSwapTransactionMetadata.value, + gasLimit: step.butterSwapTransactionMetadata.gasLimit, + } + } + + // For THORChain/MAYAChain and other swappers, transaction is built differently + // The consumer will need to use the quote data to build the transaction + return undefined +} + +// Helper to build approval info +const buildApprovalInfo = (step: TradeQuoteStep, _sellAssetId: string): ApprovalInfo => { + const { chainNamespace } = fromChainId(step.sellAsset.chainId) + + // Only EVM tokens need approval + if (chainNamespace !== 'eip155') { + return { isRequired: false, spender: '' } + } + + // Native assets don't need approval + if (step.sellAsset.assetId.includes('slip44:60')) { + return { isRequired: false, spender: '' } + } + + // If there's an allowance contract, approval may be needed + if (step.allowanceContract) { + return { + isRequired: true, // Consumer should check current allowance + spender: step.allowanceContract, + // Approval transaction data - consumer builds this themselves + // or we could provide it if needed + } + } + + return { isRequired: false, spender: '' } +} + +// Transform quote step to API format +const transformQuoteStep = (step: TradeQuoteStep): ApiQuoteStep => ({ + sellAsset: step.sellAsset, + buyAsset: step.buyAsset, + sellAmountCryptoBaseUnit: step.sellAmountIncludingProtocolFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit: step.buyAmountAfterFeesCryptoBaseUnit, + allowanceContract: step.allowanceContract, + estimatedExecutionTimeMs: step.estimatedExecutionTimeMs, + source: step.source, + transactionData: extractTransactionData(step), +}) + +export const getQuote = async (req: Request, res: Response): Promise => { + try { + // Parse and validate request + const parseResult = QuoteRequestSchema.safeParse(req.body) + if (!parseResult.success) { + const errorResponse: ErrorResponse = { + error: 'Invalid request parameters', + details: parseResult.error.errors, + } + res.status(400).json(errorResponse) + return + } + + const { + sellAssetId, + buyAssetId, + sellAmountCryptoBaseUnit, + receiveAddress, + sendAddress, + swapperName, + slippageTolerancePercentageDecimal, + allowMultiHop, + accountNumber, + } = parseResult.data + + // Lazy load swapper module + const { getTradeQuotes, swappers, SwapperName, getDefaultSlippageDecimalPercentageForSwapper } = + await getSwapperModule() + + // Validate swapper name + const validSwapperName = Object.values(SwapperName).find(v => v === swapperName) as + | SwapperName + | undefined + if (!validSwapperName) { + res.status(400).json({ error: `Unknown swapper: ${swapperName}` } as ErrorResponse) + return + } + + // Validate swapper exists + const swapper = swappers[validSwapperName] + if (!swapper) { + res.status(400).json({ error: `Swapper not available: ${swapperName}` } as ErrorResponse) + return + } + + // Get assets + const sellAsset = getAsset(sellAssetId) + const buyAsset = getAsset(buyAssetId) + + if (!sellAsset) { + res.status(400).json({ error: `Unknown sell asset: ${sellAssetId}` } as ErrorResponse) + return + } + if (!buyAsset) { + res.status(400).json({ error: `Unknown buy asset: ${buyAssetId}` } as ErrorResponse) + return + } + + // Create swapper dependencies + const deps = createServerSwapperDeps(getAssetsById()) + + // Get default slippage if not provided + let slippage = slippageTolerancePercentageDecimal + if (!slippage) { + try { + slippage = getDefaultSlippageDecimalPercentageForSwapper(validSwapperName) + } catch { + slippage = '0.01' // 1% default fallback + } + } + + // Build quote input + const quoteInput = { + sellAsset, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + affiliateBps: DEFAULT_AFFILIATE_BPS, + allowMultiHop, + slippageTolerancePercentageDecimal: slippage, + receiveAddress, + sendAddress, + accountNumber, + quoteOrRate: 'quote' as const, + chainId: sellAsset.chainId, + // EVM-specific fields + supportsEIP1559: true, + } + + // Fetch quote + const result = await getTradeQuotes( + quoteInput as GetTradeQuoteInputWithWallet, + validSwapperName, + deps, + ) + + if (!result) { + res.status(404).json({ error: 'No quote available from this swapper' } as ErrorResponse) + return + } + + if (result.isErr()) { + const error = result.unwrapErr() + res.status(400).json({ + error: error.message, + code: error.code, + details: error.details, + } as ErrorResponse) + return + } + + const quotes = result.unwrap() + if (quotes.length === 0) { + res.status(404).json({ error: 'No quote available' } as ErrorResponse) + return + } + + // Use the first/best quote + const quote = quotes[0] as TradeQuote + const firstStep = quote.steps[0] + + // Calculate total buy amount (sum of all steps for multi-hop) + const lastStep = quote.steps[quote.steps.length - 1] + + // Build response + const quoteId = uuidv4() + const now = Date.now() + + const response: QuoteResponse = { + quoteId, + swapperName: validSwapperName, + rate: quote.rate, + sellAsset, + buyAsset, + sellAmountCryptoBaseUnit: firstStep.sellAmountIncludingProtocolFeesCryptoBaseUnit, + buyAmountBeforeFeesCryptoBaseUnit: lastStep.buyAmountBeforeFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit: lastStep.buyAmountAfterFeesCryptoBaseUnit, + affiliateBps: quote.affiliateBps, + slippageTolerancePercentageDecimal: quote.slippageTolerancePercentageDecimal, + networkFeeCryptoBaseUnit: firstStep.feeData.networkFeeCryptoBaseUnit, + steps: quote.steps.map(transformQuoteStep), + approval: buildApprovalInfo(firstStep, sellAssetId), + expiresAt: now + 60_000, // 60 second expiry + quote, // Include full quote for advanced consumers + } + + res.json(response) + } catch (error) { + console.error('Error in getQuote:', error) + res.status(500).json({ error: 'Internal server error' } as ErrorResponse) + } +} diff --git a/packages/public-api/src/routes/rates.ts b/packages/public-api/src/routes/rates.ts new file mode 100644 index 00000000000..03b227f74f1 --- /dev/null +++ b/packages/public-api/src/routes/rates.ts @@ -0,0 +1,210 @@ +import type { GetTradeRateInput } from '@shapeshiftoss/swapper' +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { getAsset, getAssetsById } from '../assets' +import { DEFAULT_AFFILIATE_BPS } from '../config' +import { createServerSwapperDeps } from '../swapperDeps' +import type { ApiRate, ErrorResponse, RatesResponse } from '../types' + +// Request validation schema +export const RatesRequestSchema = z.object({ + sellAssetId: z.string().min(1).openapi({ example: 'eip155:1/slip44:60' }), + buyAssetId: z + .string() + .min(1) + .openapi({ example: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' }), + sellAmountCryptoBaseUnit: z.string().min(1).openapi({ example: '1000000000000000000' }), + slippageTolerancePercentageDecimal: z.string().optional().openapi({ example: '0.01' }), + allowMultiHop: z.boolean().optional().default(true).openapi({ example: true }), +}) + +// Swapper names as strings to avoid import issues +const ENABLED_SWAPPER_NAMES = [ + 'THORChain', + 'MAYAChain', + '0x', + 'CoW Swap', + 'Portals', + 'Chainflip', + 'Jupiter', + 'Relay', + 'ButterSwap', + 'Bebop', +] as const + +// Rate timeout per swapper (10 seconds) +const RATE_TIMEOUT_MS = 10_000 + +// Lazy load swapper to avoid import issues at module load time +let swapperModule: Awaited> | null = null +const importSwapperModule = () => import('@shapeshiftoss/swapper') +const getSwapperModule = async () => { + if (!swapperModule) { + swapperModule = await importSwapperModule() + } + return swapperModule +} + +export const getRates = async (req: Request, res: Response): Promise => { + try { + // Parse and validate request + const parseResult = RatesRequestSchema.safeParse(req.query) + if (!parseResult.success) { + const errorResponse: ErrorResponse = { + error: 'Invalid request parameters', + details: parseResult.error.errors, + } + res.status(400).json(errorResponse) + return + } + + const { + sellAssetId, + buyAssetId, + sellAmountCryptoBaseUnit, + slippageTolerancePercentageDecimal, + allowMultiHop, + } = parseResult.data + + // Get assets + const sellAsset = getAsset(sellAssetId) + const buyAsset = getAsset(buyAssetId) + + if (!sellAsset) { + res.status(400).json({ error: `Unknown sell asset: ${sellAssetId}` } as ErrorResponse) + return + } + if (!buyAsset) { + res.status(400).json({ error: `Unknown buy asset: ${buyAssetId}` } as ErrorResponse) + return + } + + // Lazy load swapper module + let swapperModuleResult + try { + swapperModuleResult = await getSwapperModule() + } catch (error) { + console.error('Failed to load swapper module:', error) + res.status(503).json({ + error: 'Swap service temporarily unavailable', + details: 'Failed to initialize swapper module', + } as ErrorResponse) + return + } + const { getTradeRates, swappers, SwapperName } = swapperModuleResult + + // Create swapper dependencies + const deps = createServerSwapperDeps(getAssetsById()) + + // Build rate input + const rateInput = { + sellAsset, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit: sellAmountCryptoBaseUnit, + affiliateBps: DEFAULT_AFFILIATE_BPS, + allowMultiHop, + slippageTolerancePercentageDecimal, + receiveAddress: undefined, + sendAddress: undefined, + accountNumber: undefined, + quoteOrRate: 'rate' as const, + chainId: sellAsset.chainId, + } + + // Map string names to SwapperName enum + const enabledSwappers = ENABLED_SWAPPER_NAMES.map(name => { + const swapperName = Object.values(SwapperName).find(v => v === name) + return swapperName + }).filter((name): name is (typeof SwapperName)[keyof typeof SwapperName] => name !== undefined) + + // Fetch rates from all enabled swappers in parallel + const ratePromises = enabledSwappers.map(async (swapperName): Promise => { + try { + const swapper = swappers[swapperName] + if (!swapper) return null + + const result = await getTradeRates( + rateInput as GetTradeRateInput, + swapperName, + deps, + RATE_TIMEOUT_MS, + ) + + if (!result) return null + + if (result.isErr()) { + const error = result.unwrapErr() + return { + swapperName, + rate: '0', + buyAmountCryptoBaseUnit: '0', + sellAmountCryptoBaseUnit, + steps: 0, + estimatedExecutionTimeMs: undefined, + priceImpactPercentageDecimal: undefined, + affiliateBps: DEFAULT_AFFILIATE_BPS, + networkFeeCryptoBaseUnit: undefined, + error: { + code: error.code || ('UnknownError' as any), + message: error.message, + }, + } + } + + const rates = result.unwrap() + if (rates.length === 0) return null + + // Return the first/best rate + const rate = rates[0] + const firstStep = rate.steps[0] + + return { + swapperName, + rate: rate.rate, + buyAmountCryptoBaseUnit: firstStep.buyAmountAfterFeesCryptoBaseUnit, + sellAmountCryptoBaseUnit: firstStep.sellAmountIncludingProtocolFeesCryptoBaseUnit, + steps: rate.steps.length, + estimatedExecutionTimeMs: firstStep.estimatedExecutionTimeMs, + priceImpactPercentageDecimal: rate.priceImpactPercentageDecimal, + affiliateBps: rate.affiliateBps, + networkFeeCryptoBaseUnit: firstStep.feeData.networkFeeCryptoBaseUnit, + } + } catch (error) { + console.error(`Error fetching rate from ${swapperName}:`, error) + return null + } + }) + + const results = await Promise.allSettled(ratePromises) + const rates: ApiRate[] = results + .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') + .map(r => r.value) + .filter((r): r is ApiRate => r !== null) + + // Sort by best rate (highest buy amount) + rates.sort((a, b) => { + if (a.error && !b.error) return 1 + if (!a.error && b.error) return -1 + try { + const aBuyAmount = BigInt(a.buyAmountCryptoBaseUnit.split('.')[0] ?? '0') + const bBuyAmount = BigInt(b.buyAmountCryptoBaseUnit.split('.')[0] ?? '0') + return bBuyAmount > aBuyAmount ? 1 : bBuyAmount < aBuyAmount ? -1 : 0 + } catch { + return 0 + } + }) + + const now = Date.now() + const response: RatesResponse = { + rates, + timestamp: now, + expiresAt: now + 30_000, // 30 second expiry + } + + res.json(response) + } catch (error) { + console.error('Error in getRates:', error) + res.status(500).json({ error: 'Internal server error' } as ErrorResponse) + } +} diff --git a/packages/public-api/src/server-standalone.ts b/packages/public-api/src/server-standalone.ts new file mode 100644 index 00000000000..b635106082d --- /dev/null +++ b/packages/public-api/src/server-standalone.ts @@ -0,0 +1,245 @@ +/** + * Standalone test server for the Public Swap API + * This version uses mocked responses to demonstrate the API structure + * without requiring full swapper package integration. + * + * For production, use index.ts which integrates with the real swapper package. + */ + +import cors from 'cors' +import crypto from 'crypto' +import express from 'express' +import { v4 as uuidv4 } from 'uuid' + +const API_PORT = parseInt(process.env.PORT || '3001', 10) +const API_HOST = process.env.HOST || '0.0.0.0' + +// Static API keys for testing +const STATIC_API_KEYS: Record = { + 'test-api-key-123': { name: 'Test Partner', feeSharePercentage: 50 }, +} + +const app = express() + +// Middleware +app.use(cors()) +app.use(express.json()) + +// API key auth middleware +const apiKeyAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => { + const apiKey = req.header('X-API-Key') + + if (!apiKey) { + return res.status(401).json({ error: 'API key required', code: 'MISSING_API_KEY' }) + } + + const partnerInfo = STATIC_API_KEYS[apiKey] + if (!partnerInfo) { + return res.status(401).json({ error: 'Invalid API key', code: 'INVALID_API_KEY' }) + } + + ;(req as any).partner = { + id: crypto.createHash('sha256').update(apiKey).digest('hex').substring(0, 16), + name: partnerInfo.name, + feeSharePercentage: partnerInfo.feeSharePercentage, + } + + next() +} + +// Health check +app.get('/health', (_req, res) => { + res.json({ status: 'ok', timestamp: Date.now() }) +}) + +// Mock rates endpoint +app.get('/v1/swap/rates', apiKeyAuth, (req, res) => { + const { sellAssetId, buyAssetId, sellAmountCryptoBaseUnit } = req.query + + if (!sellAssetId || !buyAssetId || !sellAmountCryptoBaseUnit) { + return res.status(400).json({ + error: 'Missing required parameters: sellAssetId, buyAssetId, sellAmountCryptoBaseUnit', + }) + } + + // Mock response demonstrating API structure + const mockRates = [ + { + swapperName: 'THORChain', + rate: '3500.123456', + buyAmountCryptoBaseUnit: '3500123456', + sellAmountCryptoBaseUnit: sellAmountCryptoBaseUnit as string, + steps: 1, + estimatedExecutionTimeMs: 600000, + priceImpactPercentageDecimal: '0.001', + affiliateBps: '55', + networkFeeCryptoBaseUnit: '5000000000000000', + }, + { + swapperName: '0x', + rate: '3498.789012', + buyAmountCryptoBaseUnit: '3498789012', + sellAmountCryptoBaseUnit: sellAmountCryptoBaseUnit as string, + steps: 1, + estimatedExecutionTimeMs: 30000, + priceImpactPercentageDecimal: '0.002', + affiliateBps: '55', + networkFeeCryptoBaseUnit: '3000000000000000', + }, + { + swapperName: 'CoW Swap', + rate: '3499.567890', + buyAmountCryptoBaseUnit: '3499567890', + sellAmountCryptoBaseUnit: sellAmountCryptoBaseUnit as string, + steps: 1, + estimatedExecutionTimeMs: 120000, + priceImpactPercentageDecimal: '0.0015', + affiliateBps: '55', + networkFeeCryptoBaseUnit: '2000000000000000', + }, + ] + + const now = Date.now() + res.json({ + rates: mockRates, + timestamp: now, + expiresAt: now + 30000, + }) +}) + +// Mock quote endpoint +app.post('/v1/swap/quote', apiKeyAuth, (req, res) => { + const { sellAssetId, buyAssetId, sellAmountCryptoBaseUnit, receiveAddress, swapperName } = + req.body + + if (!sellAssetId || !buyAssetId || !sellAmountCryptoBaseUnit || !receiveAddress || !swapperName) { + return res.status(400).json({ + error: 'Missing required parameters', + }) + } + + const quoteId = uuidv4() + const now = Date.now() + + // Mock response with transaction data + res.json({ + quoteId, + swapperName, + rate: '3500.123456', + sellAsset: { + assetId: sellAssetId, + chainId: 'eip155:1', + symbol: 'ETH', + name: 'Ethereum', + precision: 18, + }, + buyAsset: { + assetId: buyAssetId, + chainId: 'eip155:1', + symbol: 'USDC', + name: 'USD Coin', + precision: 6, + }, + sellAmountCryptoBaseUnit, + buyAmountBeforeFeesCryptoBaseUnit: '3505000000', + buyAmountAfterFeesCryptoBaseUnit: '3500000000', + affiliateBps: '55', + slippageTolerancePercentageDecimal: '0.01', + networkFeeCryptoBaseUnit: '5000000000000000', + steps: [ + { + sellAsset: { assetId: sellAssetId, symbol: 'ETH', precision: 18 }, + buyAsset: { assetId: buyAssetId, symbol: 'USDC', precision: 6 }, + sellAmountCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit: '3500000000', + allowanceContract: '0x0000000000000000000000000000000000000000', + estimatedExecutionTimeMs: 600000, + source: swapperName, + transactionData: { + to: '0x1111111254EEB25477B68fb85Ed929f73A960582', + data: '0x12aa3caf000000000000000000000000...', // Truncated for example + value: sellAmountCryptoBaseUnit, + gasLimit: '250000', + }, + }, + ], + approval: { + isRequired: false, + spender: '0x0000000000000000000000000000000000000000', + }, + expiresAt: now + 60000, + }) +}) + +// Mock assets endpoint +app.get('/v1/assets', (_req, res) => { + res.json({ + assets: [ + { + assetId: 'eip155:1/slip44:60', + chainId: 'eip155:1', + symbol: 'ETH', + name: 'Ethereum', + precision: 18, + }, + { + assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + chainId: 'eip155:1', + symbol: 'USDC', + name: 'USD Coin', + precision: 6, + }, + { + assetId: 'bip122:000000000019d6689c085ae165831e93/slip44:0', + chainId: 'bip122:000000000019d6689c085ae165831e93', + symbol: 'BTC', + name: 'Bitcoin', + precision: 8, + }, + ], + timestamp: Date.now(), + }) +}) + +// 404 handler +app.use((_req, res) => { + res.status(404).json({ error: 'Not found' }) +}) + +// Start server +app.listen(API_PORT, API_HOST, () => { + console.log(` +╔══════════════════════════════════════════════════════════════════╗ +║ ShapeShift Public Swap API ║ +║ (Mock/Test Server) ║ +╚══════════════════════════════════════════════════════════════════╝ + +Server running at http://${API_HOST}:${API_PORT} + +Endpoints: + GET /health - Health check (no auth) + GET /v1/swap/rates - Get swap rates from all swappers + POST /v1/swap/quote - Get executable quote with tx data + GET /v1/assets - List supported assets + +Authentication: + Include 'X-API-Key' header for /v1/swap/* endpoints + Test key: test-api-key-123 + +Example requests: + + # Health check + curl http://localhost:${API_PORT}/health + + # Get rates + curl -H "X-API-Key: test-api-key-123" \\ + "http://localhost:${API_PORT}/v1/swap/rates?sellAssetId=eip155:1/slip44:60&buyAssetId=eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&sellAmountCryptoBaseUnit=1000000000000000000" + + # Get quote + curl -X POST -H "X-API-Key: test-api-key-123" -H "Content-Type: application/json" \\ + -d '{"sellAssetId":"eip155:1/slip44:60","buyAssetId":"eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","sellAmountCryptoBaseUnit":"1000000000000000000","receiveAddress":"0x742d35Cc6634C0532925a3b844Bc9e7595f4EdC3","swapperName":"THORChain"}' \\ + http://localhost:${API_PORT}/v1/swap/quote + +Note: This is a mock server for testing. Real integration uses index.ts. +`) +}) diff --git a/packages/public-api/src/setupZod.ts b/packages/public-api/src/setupZod.ts new file mode 100644 index 00000000000..ecef04a8792 --- /dev/null +++ b/packages/public-api/src/setupZod.ts @@ -0,0 +1,4 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi' +import { z } from 'zod' + +extendZodWithOpenApi(z) diff --git a/packages/public-api/src/swapperDeps.ts b/packages/public-api/src/swapperDeps.ts new file mode 100644 index 00000000000..26e0635cc62 --- /dev/null +++ b/packages/public-api/src/swapperDeps.ts @@ -0,0 +1,154 @@ +import type { ChainId } from '@shapeshiftoss/caip' +import type { + ChainAdapter, + CosmosSdkChainAdapter, + EvmChainAdapter, + near, + solana, + starknet, + sui, + tron, + UtxoChainAdapter, +} from '@shapeshiftoss/chain-adapters' +import type { SwapperDeps } from '@shapeshiftoss/swapper' +import type { AssetsByIdPartial } from '@shapeshiftoss/types' +import { KnownChainIds } from '@shapeshiftoss/types' + +import { getServerConfig } from './config' + +type GasFeeData = { + gasPrice: string + maxFeePerGas?: string + maxPriorityFeePerGas?: string +} + +type GasFeeDataEstimate = { + fast: GasFeeData + average: GasFeeData + slow: GasFeeData +} + +const EVM_UNCHAINED_URLS: Record = { + [KnownChainIds.EthereumMainnet]: 'https://api.ethereum.shapeshift.com', + [KnownChainIds.ArbitrumMainnet]: 'https://api.arbitrum.shapeshift.com', + [KnownChainIds.OptimismMainnet]: 'https://api.optimism.shapeshift.com', + [KnownChainIds.PolygonMainnet]: 'https://api.polygon.shapeshift.com', + [KnownChainIds.GnosisMainnet]: 'https://api.gnosis.shapeshift.com', + [KnownChainIds.AvalancheMainnet]: 'https://api.avalanche.shapeshift.com', + [KnownChainIds.BnbSmartChainMainnet]: 'https://api.bnbsmartchain.shapeshift.com', + [KnownChainIds.BaseMainnet]: 'https://api.base.shapeshift.com', + [KnownChainIds.ArbitrumNovaMainnet]: 'https://api.arbitrum-nova.shapeshift.com', +} + +const fetchGasFees = async (unchainedUrl: string): Promise => { + const response = await fetch(`${unchainedUrl}/api/v1/gas/fees`) + if (!response.ok) { + throw new Error(`Failed to fetch gas fees: ${response.statusText}`) + } + const data = (await response.json()) as GasFeeDataEstimate + return { + fast: { + gasPrice: data.fast.gasPrice, + maxFeePerGas: data.fast.maxFeePerGas, + maxPriorityFeePerGas: data.fast.maxPriorityFeePerGas, + }, + average: { + gasPrice: data.average.gasPrice, + maxFeePerGas: data.average.maxFeePerGas, + maxPriorityFeePerGas: data.average.maxPriorityFeePerGas, + }, + slow: { + gasPrice: data.slow.gasPrice, + maxFeePerGas: data.slow.maxFeePerGas, + maxPriorityFeePerGas: data.slow.maxPriorityFeePerGas, + }, + } +} + +const createMinimalEvmAdapter = (chainId: ChainId) => { + const unchainedUrl = EVM_UNCHAINED_URLS[chainId] + if (!unchainedUrl) { + throw new Error(`No Unchained URL configured for chain ${chainId}`) + } + + return { + getChainId: () => chainId, + getGasFeeData: () => fetchGasFees(unchainedUrl), + getFeeAssetId: () => { + switch (chainId) { + case KnownChainIds.EthereumMainnet: + return 'eip155:1/slip44:60' + case KnownChainIds.ArbitrumMainnet: + return 'eip155:42161/slip44:60' + case KnownChainIds.OptimismMainnet: + return 'eip155:10/slip44:60' + case KnownChainIds.PolygonMainnet: + return 'eip155:137/slip44:966' + case KnownChainIds.GnosisMainnet: + return 'eip155:100/slip44:700' + case KnownChainIds.AvalancheMainnet: + return 'eip155:43114/slip44:9005' + case KnownChainIds.BnbSmartChainMainnet: + return 'eip155:56/slip44:714' + case KnownChainIds.BaseMainnet: + return 'eip155:8453/slip44:60' + case KnownChainIds.ArbitrumNovaMainnet: + return 'eip155:42170/slip44:60' + default: + return `${chainId}/slip44:60` + } + }, + getDisplayName: () => chainId, + } +} + +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.`, + ) + } +} + +export const createServerSwapperDeps = (assetsById: AssetsByIdPartial): SwapperDeps => ({ + assetsById, + config: getServerConfig(), + mixPanel: undefined, + + assertGetChainAdapter: createStubAdapter('generic') as unknown as ( + chainId: ChainId, + ) => ChainAdapter, + + assertGetEvmChainAdapter: ((chainId: ChainId) => { + const unchainedUrl = EVM_UNCHAINED_URLS[chainId] + if (unchainedUrl) { + return createMinimalEvmAdapter(chainId) + } + throw new Error(`Chain adapter EVM for ${chainId} not implemented in public API.`) + }) as unknown as (chainId: ChainId) => EvmChainAdapter, + + assertGetUtxoChainAdapter: createStubAdapter('UTXO') as unknown as ( + chainId: ChainId, + ) => UtxoChainAdapter, + assertGetCosmosSdkChainAdapter: createStubAdapter('CosmosSdk') as unknown as ( + chainId: ChainId, + ) => CosmosSdkChainAdapter, + assertGetSolanaChainAdapter: createStubAdapter('Solana') as unknown as ( + chainId: ChainId, + ) => solana.ChainAdapter, + assertGetTronChainAdapter: createStubAdapter('Tron') as unknown as ( + chainId: ChainId, + ) => tron.ChainAdapter, + assertGetSuiChainAdapter: createStubAdapter('Sui') as unknown as ( + chainId: ChainId, + ) => sui.ChainAdapter, + assertGetNearChainAdapter: createStubAdapter('Near') as unknown as ( + chainId: ChainId, + ) => near.ChainAdapter, + assertGetStarknetChainAdapter: createStubAdapter('Starknet') as unknown as ( + chainId: ChainId, + ) => starknet.ChainAdapter, + + fetchIsSmartContractAddressQuery: () => Promise.resolve(false), +}) diff --git a/packages/public-api/src/types.ts b/packages/public-api/src/types.ts new file mode 100644 index 00000000000..edcdbde5a61 --- /dev/null +++ b/packages/public-api/src/types.ts @@ -0,0 +1,138 @@ +import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import type { SwapperName, TradeQuote, TradeQuoteError, TradeRate } from '@shapeshiftoss/swapper' +import type { Asset } from '@shapeshiftoss/types' + +// Partner configuration +export type PartnerConfig = { + id: string + apiKeyHash: string + name: string + feeSharePercentage: number + status: 'active' | 'suspended' | 'pending' + rateLimit: { + requestsPerMinute: number + requestsPerDay: number + } + createdAt: Date +} + +// API Request Types +export type RatesRequest = { + sellAssetId: AssetId + buyAssetId: AssetId + sellAmountCryptoBaseUnit: string + slippageTolerancePercentageDecimal?: string + allowMultiHop?: boolean +} + +export type QuoteRequest = { + sellAssetId: AssetId + buyAssetId: AssetId + sellAmountCryptoBaseUnit: string + receiveAddress: string + sendAddress?: string + swapperName: SwapperName + slippageTolerancePercentageDecimal?: string + allowMultiHop?: boolean + accountNumber?: number +} + +export type StatusRequest = { + txHash: string + chainId: ChainId + swapperName: SwapperName +} + +// API Response Types +export type ApiRate = { + swapperName: SwapperName + rate: string + buyAmountCryptoBaseUnit: string + sellAmountCryptoBaseUnit: string + steps: number + estimatedExecutionTimeMs: number | undefined + priceImpactPercentageDecimal: string | undefined + affiliateBps: string + networkFeeCryptoBaseUnit: string | undefined + error?: { + code: TradeQuoteError + message: string + } +} + +export type RatesResponse = { + rates: ApiRate[] + timestamp: number + expiresAt: number +} + +export type ApiQuoteStep = { + sellAsset: Asset + buyAsset: Asset + sellAmountCryptoBaseUnit: string + buyAmountAfterFeesCryptoBaseUnit: string + allowanceContract: string + estimatedExecutionTimeMs: number | undefined + source: string + transactionData?: { + to: string + data: string + value: string + gasLimit?: string + } +} + +export type ApprovalInfo = { + isRequired: boolean + spender: string + approvalTx?: { + to: string + data: string + value: string + } +} + +export type QuoteResponse = { + quoteId: string + swapperName: SwapperName + rate: string + sellAsset: Asset + buyAsset: Asset + sellAmountCryptoBaseUnit: string + buyAmountBeforeFeesCryptoBaseUnit: string + buyAmountAfterFeesCryptoBaseUnit: string + affiliateBps: string + slippageTolerancePercentageDecimal: string | undefined + networkFeeCryptoBaseUnit: string | undefined + steps: ApiQuoteStep[] + approval: ApprovalInfo + expiresAt: number + quote: TradeQuote | TradeRate +} + +export type StatusResponse = { + status: 'pending' | 'confirmed' | 'failed' | 'unknown' + sellTxHash: string + buyTxHash?: string + message?: string +} + +export type AssetsResponse = { + assets: Asset[] + timestamp: number +} + +export type ErrorResponse = { + error: string + code?: string + details?: unknown +} + +// Extend Express Request to include partner info +declare global { + namespace Express { + interface Request { + partner?: PartnerConfig + } + } +} diff --git a/packages/public-api/tests/run-smoke-tests.ts b/packages/public-api/tests/run-smoke-tests.ts new file mode 100644 index 00000000000..7510bad22ad --- /dev/null +++ b/packages/public-api/tests/run-smoke-tests.ts @@ -0,0 +1,76 @@ +import { runSmokeTests } from './smoke-tests' +import { sleep } from './test-utils' + +const main = async (): Promise => { + console.log('='.repeat(60)) + console.log('ShapeShift Public API - Smoke Tests') + console.log('='.repeat(60)) + + const apiUrl = process.env.API_URL || 'http://localhost:3001' + console.log(`Testing API at: ${apiUrl}`) + console.log('') + + // Wait for API to be ready (Railway may still be spinning up) + const maxRetries = 10 + const retryDelay = 3000 + + for (let i = 0; i < maxRetries; i++) { + try { + const res = await fetch(`${apiUrl}/health`) + if (res.ok) { + console.log('API is ready!') + break + } + } catch { + // Ignore - API not ready yet + } + + if (i < maxRetries - 1) { + console.log(`Waiting for API to be ready... (${i + 1}/${maxRetries})`) + await sleep(retryDelay) + } else { + console.error('API did not become ready in time') + // Still exit 0 to not block deployment + process.exit(0) + } + } + + console.log('') + const results = await runSmokeTests() + + // Print results + console.log('\nTest Results:') + console.log('-'.repeat(60)) + + for (const result of results.results) { + const status = result.passed ? 'PASS' : result.critical ? 'FAIL (CRITICAL)' : 'FAIL (warning)' + const icon = result.passed ? '[OK]' : '[!!]' + console.log(`${icon} ${result.name}: ${status} (${result.duration}ms)`) + if (result.error) { + console.log(` Error: ${result.error}`) + } + } + + console.log('-'.repeat(60)) + console.log( + `Total: ${results.totalTests} | Passed: ${results.passed} | Failed: ${results.failed}`, + ) + + if (results.criticalFailures > 0) { + console.log(`\n[WARNING] ${results.criticalFailures} CRITICAL test(s) failed!`) + console.log('The API may not be functioning correctly.') + } + + // Output JSON for programmatic consumption + console.log('\n--- JSON Results ---') + console.log(JSON.stringify(results, null, 2)) + + // Always exit 0 to not block deployment + // Critical failures are logged but don't fail the deploy + process.exit(0) +} + +main().catch(err => { + console.error('Smoke test runner error:', err) + process.exit(0) // Still exit 0 to not block +}) diff --git a/packages/public-api/tests/smoke-tests.ts b/packages/public-api/tests/smoke-tests.ts new file mode 100644 index 00000000000..caa752fd85b --- /dev/null +++ b/packages/public-api/tests/smoke-tests.ts @@ -0,0 +1,203 @@ +import { ASSET_IDS, TEST_API_KEY, TEST_PAIRS } from './test-config' +import type { TestResult, TestSuiteResult } from './test-utils' +import { fetchWithTimeout, runTest } from './test-utils' + +const getApiUrl = (): string => process.env.API_URL || 'http://localhost:3001' + +export const runSmokeTests = async (): Promise => { + const API_URL = getApiUrl() + const results: TestResult[] = [] + + // 1. Health Check (Critical) + results.push( + await runTest('Health check', true, async () => { + const res = await fetchWithTimeout(`${API_URL}/health`, {}) + if (!res.ok) throw new Error(`Health check failed: ${res.status}`) + const data = (await res.json()) as { status?: string } + if (data.status !== 'ok') throw new Error('Health status not ok') + }), + ) + + // 2. Asset Count (Critical) + results.push( + await runTest('Asset count', true, async () => { + const res = await fetchWithTimeout(`${API_URL}/v1/assets/count`, {}) + if (!res.ok) throw new Error(`Asset count failed: ${res.status}`) + const data = (await res.json()) as { count?: number } + if (typeof data.count !== 'number' || data.count === 0) { + throw new Error(`Invalid asset count: ${data.count}`) + } + }), + ) + + // 3. Asset List (Critical) + results.push( + await runTest('Asset list', true, async () => { + const res = await fetchWithTimeout(`${API_URL}/v1/assets?limit=10`, {}) + if (!res.ok) throw new Error(`Asset list failed: ${res.status}`) + const data = (await res.json()) as { assets?: unknown[] } + if (!Array.isArray(data.assets) || data.assets.length === 0) { + throw new Error('No assets returned') + } + }), + ) + + // 4. Single Asset Lookup (Critical) + results.push( + await runTest('Single asset lookup (ETH)', true, async () => { + const assetId = encodeURIComponent(ASSET_IDS.ETH) + const res = await fetchWithTimeout(`${API_URL}/v1/assets/${assetId}`, {}) + if (!res.ok) throw new Error(`Asset lookup failed: ${res.status}`) + const asset = (await res.json()) as { chainId?: string; symbol?: string } + if (!asset.chainId || !asset.symbol) { + throw new Error('Invalid asset structure') + } + }), + ) + + // 5. Rates Auth Check (Critical) + results.push( + await runTest('Rates requires auth', true, async () => { + const params = new URLSearchParams({ + sellAssetId: ASSET_IDS.ETH, + buyAssetId: ASSET_IDS.USDC_ETH, + sellAmountCryptoBaseUnit: '100000000000000000', + }) + const res = await fetchWithTimeout(`${API_URL}/v1/swap/rates?${params}`, {}) + if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`) + }), + ) + + // 6. Quote Auth Check (Critical) + results.push( + await runTest('Quote requires auth', true, async () => { + const res = await fetchWithTimeout(`${API_URL}/v1/swap/quote`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sellAssetId: ASSET_IDS.ETH, + buyAssetId: ASSET_IDS.USDC_ETH, + sellAmountCryptoBaseUnit: '100000000000000000', + receiveAddress: '0x0000000000000000000000000000000000000000', + swapperName: '0x', + }), + }) + if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`) + }), + ) + + // 7. EVM Same-Chain Rates (Informative) + const evmPair = TEST_PAIRS.evmSameChain[0] + results.push( + await runTest(`Rates: ${evmPair.name}`, false, async () => { + const params = new URLSearchParams({ + sellAssetId: evmPair.sellAssetId, + buyAssetId: evmPair.buyAssetId, + sellAmountCryptoBaseUnit: evmPair.sellAmountCryptoBaseUnit, + }) + const res = await fetchWithTimeout( + `${API_URL}/v1/swap/rates?${params}`, + { + headers: { 'X-API-Key': TEST_API_KEY }, + }, + 30000, + ) // 30s timeout for rates + + if (!res.ok) throw new Error(`Rates request failed: ${res.status}`) + const data = (await res.json()) as { + rates?: { + swapperName: string + error?: unknown + buyAmountCryptoBaseUnit?: string + }[] + } + if (!Array.isArray(data.rates)) throw new Error('Invalid rates response structure') + + const validRates = data.rates.filter( + r => !r.error && r.buyAmountCryptoBaseUnit && r.buyAmountCryptoBaseUnit !== '0', + ) + if (validRates.length === 0) { + const swappersWithErrors = data.rates + .filter(r => r.error) + .map(r => r.swapperName) + .join(', ') + console.warn( + `Warning: No valid rates returned for ${evmPair.name}. Swappers with errors: ${ + swappersWithErrors || 'none' + }`, + ) + } else { + console.log( + ` Found ${validRates.length} valid rate(s) from: ${validRates + .map(r => r.swapperName) + .join(', ')}`, + ) + } + }), + ) + + // 8. Cross-Chain Rates (Informative) + const crossChainPair = TEST_PAIRS.crossChain[0] + results.push( + await runTest(`Rates: ${crossChainPair.name}`, false, async () => { + const params = new URLSearchParams({ + sellAssetId: crossChainPair.sellAssetId, + buyAssetId: crossChainPair.buyAssetId, + sellAmountCryptoBaseUnit: crossChainPair.sellAmountCryptoBaseUnit, + }) + const res = await fetchWithTimeout( + `${API_URL}/v1/swap/rates?${params}`, + { + headers: { 'X-API-Key': TEST_API_KEY }, + }, + 30000, + ) + + if (!res.ok) throw new Error(`Rates request failed: ${res.status}`) + const data = (await res.json()) as { + rates?: { + swapperName: string + error?: unknown + buyAmountCryptoBaseUnit?: string + }[] + } + if (!Array.isArray(data.rates)) throw new Error('Invalid rates response structure') + + const validRates = data.rates.filter( + r => !r.error && r.buyAmountCryptoBaseUnit && r.buyAmountCryptoBaseUnit !== '0', + ) + if (validRates.length === 0) { + const swappersWithErrors = data.rates + .filter(r => r.error) + .map(r => r.swapperName) + .join(', ') + console.warn( + `Warning: No valid rates returned for ${crossChainPair.name}. Swappers with errors: ${ + swappersWithErrors || 'none' + }`, + ) + } else { + console.log( + ` Found ${validRates.length} valid rate(s) from: ${validRates + .map(r => r.swapperName) + .join(', ')}`, + ) + } + }), + ) + + // Calculate summary + const passed = results.filter(r => r.passed).length + const failed = results.filter(r => !r.passed).length + const criticalFailures = results.filter(r => !r.passed && r.critical).length + + return { + timestamp: Date.now(), + apiUrl: API_URL, + totalTests: results.length, + passed, + failed, + criticalFailures, + results, + } +} diff --git a/packages/public-api/tests/test-config.ts b/packages/public-api/tests/test-config.ts new file mode 100644 index 00000000000..7ce270ba947 --- /dev/null +++ b/packages/public-api/tests/test-config.ts @@ -0,0 +1,62 @@ +export const TEST_API_KEY = 'test-api-key-123' + +export const ASSET_IDS = { + // EVM Native Assets + ETH: 'eip155:1/slip44:60', + ARB_ETH: 'eip155:42161/slip44:60', + BASE_ETH: 'eip155:8453/slip44:60', + + // EVM ERC20 Tokens + USDC_ETH: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + USDC_ARB: 'eip155:42161/erc20:0xaf88d065e77c8cc2239327c5edb3a432268e5831', + FOX_ETH: 'eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d', + + // UTXO + BTC: 'bip122:000000000019d6689c085ae165831e93/slip44:0', + DOGE: 'bip122:00000000001a91e3dace36e2be3bf030/slip44:3', + + // Cosmos + ATOM: 'cosmos:cosmoshub-4/slip44:118', + RUNE: 'cosmos:thorchain-1/slip44:931', +} as const + +export type TestPair = { + name: string + sellAssetId: string + buyAssetId: string + sellAmountCryptoBaseUnit: string + expectedSwappers: string[] +} + +export const TEST_PAIRS: Record = { + evmSameChain: [ + { + name: 'ETH to USDC (Ethereum)', + sellAssetId: ASSET_IDS.ETH, + buyAssetId: ASSET_IDS.USDC_ETH, + sellAmountCryptoBaseUnit: '100000000000000000', // 0.1 ETH + expectedSwappers: ['0x', 'CoW Swap', 'Portals', 'Bebop', 'ButterSwap'], + }, + ], + crossChain: [ + { + name: 'ETH to BTC (THORChain/Chainflip)', + sellAssetId: ASSET_IDS.ETH, + buyAssetId: ASSET_IDS.BTC, + sellAmountCryptoBaseUnit: '100000000000000000', // 0.1 ETH + expectedSwappers: ['THORChain', 'Chainflip', 'Relay'], + }, + ], +} + +export const CRITICAL_SWAPPERS = ['THORChain', '0x'] +export const NON_CRITICAL_SWAPPERS = [ + 'CoW Swap', + 'Portals', + 'Bebop', + 'ButterSwap', + 'Jupiter', + 'Chainflip', + 'MAYAChain', + 'Relay', +] diff --git a/packages/public-api/tests/test-utils.ts b/packages/public-api/tests/test-utils.ts new file mode 100644 index 00000000000..cb207104c85 --- /dev/null +++ b/packages/public-api/tests/test-utils.ts @@ -0,0 +1,55 @@ +export type TestResult = { + name: string + passed: boolean + critical: boolean + duration: number + error?: string + details?: unknown +} + +export type TestSuiteResult = { + timestamp: number + apiUrl: string + totalTests: number + passed: number + failed: number + criticalFailures: number + results: TestResult[] +} + +export const fetchWithTimeout = async ( + url: string, + options: RequestInit = {}, + timeoutMs = 15000, +): Promise => { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), timeoutMs) + + try { + return await fetch(url, { ...options, signal: controller.signal }) + } finally { + clearTimeout(timeout) + } +} + +export const runTest = async ( + name: string, + critical: boolean, + testFn: () => Promise, +): Promise => { + const start = Date.now() + try { + await testFn() + return { name, passed: true, critical, duration: Date.now() - start } + } catch (error) { + return { + name, + passed: false, + critical, + duration: Date.now() - start, + error: error instanceof Error ? error.message : String(error), + } + } +} + +export const sleep = (ms: number): Promise => new Promise(r => setTimeout(r, ms)) diff --git a/packages/public-api/tsconfig.json b/packages/public-api/tsconfig.json new file mode 100644 index 00000000000..34e59d9b831 --- /dev/null +++ b/packages/public-api/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ES2022", + "esModuleInterop": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "lib": ["ES2022"], + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/swapper/package.json b/packages/swapper/package.json index 8a7d5347773..cd00aa29cb5 100644 --- a/packages/swapper/package.json +++ b/packages/swapper/package.json @@ -36,6 +36,7 @@ "@cowprotocol/app-data": "^2.3.0", "@defuse-protocol/one-click-sdk-typescript": "^0.1.1-0.2", "@jup-ag/api": "^6.0.30", + "@mysten/sui": "^1.45.2", "@shapeshiftoss/bitcoinjs-lib": "7.0.0-shapeshift.0", "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", diff --git a/packages/swapper/src/swappers/CetusSwapper/endpoints.ts b/packages/swapper/src/swappers/CetusSwapper/endpoints.ts index 74abaa62c16..3a5c2875d0d 100644 --- a/packages/swapper/src/swappers/CetusSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/CetusSwapper/endpoints.ts @@ -1,5 +1,5 @@ import { getProvidersExcluding } from '@cetusprotocol/aggregator-sdk' -import { Transaction } from '@cetusprotocol/aggregator-sdk/node_modules/@mysten/sui/transactions' +import { Transaction } from '@mysten/sui/transactions' import { TxStatus } from '@shapeshiftoss/unchained-client' import { bnOrZero } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' diff --git a/packages/swapper/src/swappers/CetusSwapper/swapperApi/getTradeQuote.ts b/packages/swapper/src/swappers/CetusSwapper/swapperApi/getTradeQuote.ts index 6841ae00fc5..44957f76228 100644 --- a/packages/swapper/src/swappers/CetusSwapper/swapperApi/getTradeQuote.ts +++ b/packages/swapper/src/swappers/CetusSwapper/swapperApi/getTradeQuote.ts @@ -1,4 +1,4 @@ -import { Transaction } from '@cetusprotocol/aggregator-sdk/node_modules/@mysten/sui/transactions' +import { Transaction } from '@mysten/sui/transactions' import { bnOrZero } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' import { Err } from '@sniptt/monads' diff --git a/packages/swapper/src/swappers/CetusSwapper/swapperApi/getTradeRate.ts b/packages/swapper/src/swappers/CetusSwapper/swapperApi/getTradeRate.ts index 82aa09f4978..7cc7e0025fa 100644 --- a/packages/swapper/src/swappers/CetusSwapper/swapperApi/getTradeRate.ts +++ b/packages/swapper/src/swappers/CetusSwapper/swapperApi/getTradeRate.ts @@ -1,4 +1,4 @@ -import { Transaction } from '@cetusprotocol/aggregator-sdk/node_modules/@mysten/sui/transactions' +import { Transaction } from '@mysten/sui/transactions' import { bnOrZero } from '@shapeshiftoss/utils' import type { Result } from '@sniptt/monads' import { Err } from '@sniptt/monads' diff --git a/packages/unchained-client/generator/post_process.sh b/packages/unchained-client/generator/post_process.sh index e292c9080ad..67e53a0a7d3 100755 --- a/packages/unchained-client/generator/post_process.sh +++ b/packages/unchained-client/generator/post_process.sh @@ -1,3 +1,5 @@ +#!/bin/sh + # post process script to add // @ts-nocheck to the start of each generated file # there is a PR active to bake this into the generator that we can upgrade to when released: # https://github.com/OpenAPITools/openapi-generator/pull/11674 @@ -10,4 +12,4 @@ if [ "$(uname)" != "Darwin" ]; then else sed -i '' "$SED_COMMAND_1" $1 sed -i '' "$SED_COMMAND_2" $1 -fi \ No newline at end of file +fi diff --git a/packages/unchained-client/package.json b/packages/unchained-client/package.json index 56b572a8b24..de19cc3615b 100644 --- a/packages/unchained-client/package.json +++ b/packages/unchained-client/package.json @@ -22,7 +22,9 @@ }, "scripts": { "build": "yarn clean && yarn generate && yarn run -T tsc --build && yarn postbuild", + "build:docker": "yarn clean:dist && yarn run -T tsc --build && yarn postbuild", "clean": "rm -rf dist src/generated", + "clean:dist": "rm -rf dist", "dev": "yarn run -T tsc --build --watch", "generate": "TS_POST_PROCESS_FILE=./generator/post_process.sh JAVA_OPTS='-Dlog.level=error' openapi-generator-cli generate", "postbuild": "yarn postbuild:esm && yarn postbuild:cjs", diff --git a/packages/utils/package.json b/packages/utils/package.json index dfe1ad5c1f9..ec686282367 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -35,9 +35,9 @@ "@sniptt/monads": "^0.5.10", "bignumber.js": "^9.3.1", "dayjs": "^1.11.3", - "lodash": "^4.17.21" + "lodash-es": "^4.17.21" }, "devDependencies": { - "@types/lodash": "^4.14.178" + "@types/lodash-es": "^4.17.12" } } diff --git a/packages/utils/src/assetData/decodeAssetData.ts b/packages/utils/src/assetData/decodeAssetData.ts index 03eaf121236..ec102f33d16 100644 --- a/packages/utils/src/assetData/decodeAssetData.ts +++ b/packages/utils/src/assetData/decodeAssetData.ts @@ -1,7 +1,7 @@ import type { AssetId } from '@shapeshiftoss/caip' import { fromAssetId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' -import { pick } from 'lodash' +import { pick } from 'lodash-es' import { assertUnreachable } from '../assertUnreachable' import { FIELDS } from './constants' diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 96b5efd1212..909d563dc58 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,6 +1,6 @@ import type { AssetId } from '@shapeshiftoss/caip' import { ASSET_NAMESPACE, fromAssetId } from '@shapeshiftoss/caip' -import { isNull, isUndefined } from 'lodash' +import { isNull, isUndefined } from 'lodash-es' export * from './assertUnreachable' export * from './assetData' diff --git a/railway.public-api.toml b/railway.public-api.toml new file mode 100644 index 00000000000..89c10dea327 --- /dev/null +++ b/railway.public-api.toml @@ -0,0 +1,22 @@ +[build] +builder = "dockerfile" +dockerfilePath = "packages/public-api/Dockerfile" +watchPatterns = [ + "packages/public-api/**", + "packages/swapper/**", + "packages/caip/**", + "packages/types/**", + "packages/utils/**", + "packages/chain-adapters/**", + "packages/unchained-client/**", + "packages/contracts/**", + "packages/errors/**", + "public/generated/generatedAssetData.json" +] + +[deploy] +healthcheckPath = "/health" +healthcheckTimeout = 60 +startCommand = "node server.cjs" +restartPolicyType = "on_failure" +restartPolicyMaxRetries = 5 diff --git a/yarn.lock b/yarn.lock index 9c50199c7bc..40b5c0905b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -95,6 +95,17 @@ __metadata: languageName: node linkType: hard +"@asteasolutions/zod-to-openapi@npm:^7.3.4": + version: 7.3.4 + resolution: "@asteasolutions/zod-to-openapi@npm:7.3.4" + dependencies: + openapi3-ts: ^4.1.2 + peerDependencies: + zod: ^3.20.2 + checksum: 897568b35a5a317c91cf9419d49d9ce8ab1eb8e97253b0d11149032f62a5f692b27abefe7a081717225ac5b49b14df275ae691219a09605a8c347949375a1554 + languageName: node + linkType: hard + "@avnu/avnu-sdk@npm:^4.0.1": version: 4.0.1 resolution: "@avnu/avnu-sdk@npm:4.0.1" @@ -5367,6 +5378,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/aix-ppc64@npm:0.27.2" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/android-arm64@npm:0.24.2" @@ -5388,6 +5406,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm64@npm:0.27.2" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/android-arm@npm:0.24.2" @@ -5409,6 +5434,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm@npm:0.27.2" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/android-x64@npm:0.24.2" @@ -5430,6 +5462,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-x64@npm:0.27.2" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/darwin-arm64@npm:0.24.2" @@ -5451,6 +5490,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-arm64@npm:0.27.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/darwin-x64@npm:0.24.2" @@ -5472,6 +5518,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-x64@npm:0.27.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/freebsd-arm64@npm:0.24.2" @@ -5493,6 +5546,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-arm64@npm:0.27.2" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/freebsd-x64@npm:0.24.2" @@ -5514,6 +5574,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-x64@npm:0.27.2" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-arm64@npm:0.24.2" @@ -5535,6 +5602,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm64@npm:0.27.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-arm@npm:0.24.2" @@ -5556,6 +5630,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm@npm:0.27.2" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-ia32@npm:0.24.2" @@ -5577,6 +5658,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ia32@npm:0.27.2" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-loong64@npm:0.24.2" @@ -5598,6 +5686,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-loong64@npm:0.27.2" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-mips64el@npm:0.24.2" @@ -5619,6 +5714,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-mips64el@npm:0.27.2" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-ppc64@npm:0.24.2" @@ -5640,6 +5742,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ppc64@npm:0.27.2" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-riscv64@npm:0.24.2" @@ -5661,6 +5770,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-riscv64@npm:0.27.2" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-s390x@npm:0.24.2" @@ -5682,6 +5798,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-s390x@npm:0.27.2" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/linux-x64@npm:0.24.2" @@ -5703,6 +5826,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-x64@npm:0.27.2" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/netbsd-arm64@npm:0.24.2" @@ -5724,6 +5854,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-arm64@npm:0.27.2" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/netbsd-x64@npm:0.24.2" @@ -5745,6 +5882,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-x64@npm:0.27.2" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/openbsd-arm64@npm:0.24.2" @@ -5766,6 +5910,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-arm64@npm:0.27.2" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/openbsd-x64@npm:0.24.2" @@ -5787,6 +5938,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-x64@npm:0.27.2" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openharmony-arm64@npm:0.27.2" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/sunos-x64@npm:0.24.2" @@ -5808,6 +5973,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/sunos-x64@npm:0.27.2" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/win32-arm64@npm:0.24.2" @@ -5829,6 +6001,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-arm64@npm:0.27.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/win32-ia32@npm:0.24.2" @@ -5850,6 +6029,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-ia32@npm:0.27.2" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.24.2": version: 0.24.2 resolution: "@esbuild/win32-x64@npm:0.24.2" @@ -5871,6 +6057,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-x64@npm:0.27.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -8929,7 +9122,7 @@ __metadata: languageName: node linkType: hard -"@mysten/sui@npm:^1.0.5, @mysten/sui@npm:^1.3.0": +"@mysten/sui@npm:^1.0.5, @mysten/sui@npm:^1.3.0, @mysten/sui@npm:^1.45.2": version: 1.45.2 resolution: "@mysten/sui@npm:1.45.2" dependencies: @@ -11042,6 +11235,43 @@ __metadata: languageName: node linkType: hard +"@scalar/core@npm:0.3.28": + version: 0.3.28 + resolution: "@scalar/core@npm:0.3.28" + dependencies: + "@scalar/types": 0.5.4 + checksum: e585d454d09c8b12ddc196eaa20a23c55ac78de3590455d2fc289cf87d036d028af2de547cf92a9e3f41fa9497be8b568de42be8789f334576905ea6b7baf882 + languageName: node + linkType: hard + +"@scalar/express-api-reference@npm:^0.8.30": + version: 0.8.30 + resolution: "@scalar/express-api-reference@npm:0.8.30" + dependencies: + "@scalar/core": 0.3.28 + checksum: ae0fdded4fb85878bfdffa264511c4f49a1444d407353f8a53a7aba016df405d4acabbeac070f7b750c13d3a4096b4755d001af1a8f7949256f0a49bf7f9568a + languageName: node + linkType: hard + +"@scalar/helpers@npm:0.2.4": + version: 0.2.4 + resolution: "@scalar/helpers@npm:0.2.4" + checksum: 3b1ac375ce74536ab9d650257a3a2d6514a9be8e1c8158a12bf46b06ed4299b57fb90631155ded753460c9fbbcdba1a65ad0b2d743d5dc937f49bebcadcf58dc + languageName: node + linkType: hard + +"@scalar/types@npm:0.5.4": + version: 0.5.4 + resolution: "@scalar/types@npm:0.5.4" + dependencies: + "@scalar/helpers": 0.2.4 + nanoid: 5.1.5 + type-fest: 5.0.0 + zod: ^4.1.11 + checksum: 91263d099ac4460bb7f71dff54b561f2f7195157f467dafb015f78cbde01666767e1b3430ae40e1c1f1a4d5025aa0bc8d6734e1cd6bb45636303b8996a78a241 + languageName: node + linkType: hard + "@scure/base@npm:1.2.6, @scure/base@npm:^1.2.4, @scure/base@npm:^1.2.6, @scure/base@npm:~1.2.1": version: 1.2.6 resolution: "@scure/base@npm:1.2.6" @@ -11961,6 +12191,33 @@ __metadata: languageName: node linkType: hard +"@shapeshiftoss/public-api@workspace:packages/public-api": + version: 0.0.0-use.local + resolution: "@shapeshiftoss/public-api@workspace:packages/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/cors": ^2.8.17 + "@types/express": ^4.17.21 + "@types/node": ^22.10.4 + "@types/swagger-ui-express": ^4.1.8 + "@types/uuid": ^9.0.5 + cors: ^2.8.5 + express: ^4.21.0 + tsx: ^4.19.2 + typescript: ^5.7.3 + uuid: ^9.0.0 + yaml: ^2.8.2 + zod: 3.23.8 + languageName: unknown + linkType: soft + "@shapeshiftoss/swapper@workspace:^, @shapeshiftoss/swapper@workspace:packages/swapper": version: 0.0.0-use.local resolution: "@shapeshiftoss/swapper@workspace:packages/swapper" @@ -11971,6 +12228,7 @@ __metadata: "@cowprotocol/app-data": ^2.3.0 "@defuse-protocol/one-click-sdk-typescript": ^0.1.1-0.2 "@jup-ag/api": ^6.0.30 + "@mysten/sui": ^1.45.2 "@shapeshiftoss/bitcoinjs-lib": 7.0.0-shapeshift.0 "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/chain-adapters": "workspace:^" @@ -12065,10 +12323,10 @@ __metadata: "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/types": "workspace:^" "@sniptt/monads": ^0.5.10 - "@types/lodash": ^4.14.178 + "@types/lodash-es": ^4.17.12 bignumber.js: ^9.3.1 dayjs: ^1.11.3 - lodash: ^4.17.21 + lodash-es: ^4.17.21 languageName: unknown linkType: soft @@ -14101,6 +14359,16 @@ __metadata: languageName: node linkType: hard +"@types/body-parser@npm:*": + version: 1.19.6 + resolution: "@types/body-parser@npm:1.19.6" + dependencies: + "@types/connect": "*" + "@types/node": "*" + checksum: 33041e88eae00af2cfa0827e951e5f1751eafab2a8b6fce06cd89ef368a988907996436b1325180edaeddd1c0c7d0d0d4c20a6c9ff294a91e0039a9db9e9b658 + languageName: node + linkType: hard + "@types/bs58check@npm:^2.1.0": version: 2.1.0 resolution: "@types/bs58check@npm:2.1.0" @@ -14136,6 +14404,15 @@ __metadata: languageName: node linkType: hard +"@types/connect@npm:*": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "*" + checksum: 7eb1bc5342a9604facd57598a6c62621e244822442976c443efb84ff745246b10d06e8b309b6e80130026a396f19bf6793b7cecd7380169f369dac3bfc46fb99 + languageName: node + linkType: hard + "@types/connect@npm:^3.4.33": version: 3.4.35 resolution: "@types/connect@npm:3.4.35" @@ -14152,6 +14429,15 @@ __metadata: languageName: node linkType: hard +"@types/cors@npm:^2.8.17": + version: 2.8.19 + resolution: "@types/cors@npm:2.8.19" + dependencies: + "@types/node": "*" + checksum: 9545cc532c9218754443f48a0c98c1a9ba4af1fe54a3425c95de75ff3158147bb39e666cb7c6bf98cc56a9c6dc7b4ce5b2cbdae6b55d5942e50c81b76ed6b825 + languageName: node + linkType: hard + "@types/css-font-loading-module@npm:0.0.7": version: 0.0.7 resolution: "@types/css-font-loading-module@npm:0.0.7" @@ -14332,6 +14618,53 @@ __metadata: languageName: node linkType: hard +"@types/express-serve-static-core@npm:^4.17.33": + version: 4.19.7 + resolution: "@types/express-serve-static-core@npm:4.19.7" + dependencies: + "@types/node": "*" + "@types/qs": "*" + "@types/range-parser": "*" + "@types/send": "*" + checksum: 6d0f1126293a5b35d3697a5fc7e787d6c1bb8f5368dd0691fe0c8041a812a668ebb3b168124de30e600963d8acdf48ec7373daf7608d9dc1d6288f6c373d19a1 + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^5.0.0": + version: 5.1.0 + resolution: "@types/express-serve-static-core@npm:5.1.0" + dependencies: + "@types/node": "*" + "@types/qs": "*" + "@types/range-parser": "*" + "@types/send": "*" + checksum: a2a780a9954e4553b69474ea76ab5c26534dfb274e1a49524f6394cbb590c2bbf73ce9dc67ab920c25d91583c8a99d8e486696b6e9810bef7a964fcbaad88a7b + languageName: node + linkType: hard + +"@types/express@npm:*": + version: 5.0.6 + resolution: "@types/express@npm:5.0.6" + dependencies: + "@types/body-parser": "*" + "@types/express-serve-static-core": ^5.0.0 + "@types/serve-static": ^2 + checksum: da2cc3de1b1a4d7f20ed3fb6f0a8ee08e99feb3c2eb5a8d643db77017d8d0e70fee9e95da38a73f51bcdf5eda3bb6435073c0271dc04fb16fda92e55daf911fa + languageName: node + linkType: hard + +"@types/express@npm:^4.17.21": + version: 4.17.25 + resolution: "@types/express@npm:4.17.25" + dependencies: + "@types/body-parser": "*" + "@types/express-serve-static-core": ^4.17.33 + "@types/qs": "*" + "@types/serve-static": ^1 + checksum: 285d16008489d37b2be03e2e050bcf201d5d6ed9278ca13619d9029efd2055b192b2445f769116f716cfcf53d9d799a03f4e76199af9cea0ea3dee3d88595931 + languageName: node + linkType: hard + "@types/fast-text-encoding@npm:^1.0.1": version: 1.0.1 resolution: "@types/fast-text-encoding@npm:1.0.1" @@ -14376,6 +14709,13 @@ __metadata: languageName: node linkType: hard +"@types/http-errors@npm:*": + version: 2.0.5 + resolution: "@types/http-errors@npm:2.0.5" + checksum: a88da669366bc483e8f3b3eb3d34ada5f8d13eeeef851b1204d77e2ba6fc42aba4566d877cca5c095204a3f4349b87fe397e3e21288837bdd945dd514120755b + languageName: node + linkType: hard + "@types/http-proxy@npm:^1.17.8": version: 1.17.8 resolution: "@types/http-proxy@npm:1.17.8" @@ -14426,6 +14766,15 @@ __metadata: languageName: node linkType: hard +"@types/lodash-es@npm:^4.17.12": + version: 4.17.12 + resolution: "@types/lodash-es@npm:4.17.12" + dependencies: + "@types/lodash": "*" + checksum: 990a99e2243bebe9505cb5ad19fbc172beb4a8e00f9075c99fc06c46c2801ffdb40bc2867271cf580d5f48994fc9fb076ec92cd60a20e621603bf22114e5b077 + languageName: node + linkType: hard + "@types/lodash.clonedeep@npm:^4.5.7": version: 4.5.7 resolution: "@types/lodash.clonedeep@npm:4.5.7" @@ -14483,6 +14832,13 @@ __metadata: languageName: node linkType: hard +"@types/mime@npm:^1": + version: 1.3.5 + resolution: "@types/mime@npm:1.3.5" + checksum: e29a5f9c4776f5229d84e525b7cd7dd960b51c30a0fb9a028c0821790b82fca9f672dab56561e2acd9e8eed51d431bde52eafdfef30f643586c4162f1aecfc78 + languageName: node + linkType: hard + "@types/minimist@npm:^1.2.0": version: 1.2.2 resolution: "@types/minimist@npm:1.2.2" @@ -14594,6 +14950,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^22.10.4": + version: 22.19.3 + resolution: "@types/node@npm:22.19.3" + dependencies: + undici-types: ~6.21.0 + checksum: 2fffd870ac2a5a531a160034075c858e7c95c521b0612e9b7ae35e7a6bae1880c7a190a77a5c335fbb2e5f4a315c1e2f2b529f9be4bdbb2166664a7206e28951 + languageName: node + linkType: hard + "@types/node@npm:^22.13.14": version: 22.17.2 resolution: "@types/node@npm:22.17.2" @@ -14633,6 +14998,13 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:*": + version: 6.14.0 + resolution: "@types/qs@npm:6.14.0" + checksum: 1909205514d22b3cbc7c2314e2bd8056d5f05dfb21cf4377f0730ee5e338ea19957c41735d5e4806c746176563f50005bbab602d8358432e25d900bdf4970826 + languageName: node + linkType: hard + "@types/qs@npm:^6.9.18": version: 6.9.18 resolution: "@types/qs@npm:6.9.18" @@ -14640,6 +15012,13 @@ __metadata: languageName: node linkType: hard +"@types/range-parser@npm:*": + version: 1.2.7 + resolution: "@types/range-parser@npm:1.2.7" + checksum: 95640233b689dfbd85b8c6ee268812a732cf36d5affead89e806fe30da9a430767af8ef2cd661024fd97e19d61f3dec75af2df5e80ec3bea000019ab7028629a + languageName: node + linkType: hard + "@types/react-dom@npm:^19.0.0": version: 19.1.2 resolution: "@types/react-dom@npm:19.1.2" @@ -14792,6 +15171,46 @@ __metadata: languageName: node linkType: hard +"@types/send@npm:*": + version: 1.2.1 + resolution: "@types/send@npm:1.2.1" + dependencies: + "@types/node": "*" + checksum: 3b8388edeec77ae62f7bbc384c98ca06140614e4ef34fc04b35824f19937f472f8ff3785e83570e0d40e6d7c934c015d4831c82a74a1ade0d9676720835702c5 + languageName: node + linkType: hard + +"@types/send@npm:<1": + version: 0.17.6 + resolution: "@types/send@npm:0.17.6" + dependencies: + "@types/mime": ^1 + "@types/node": "*" + checksum: 5bd287f1357380963eb4b12daef5c8982f52a3269308ff3414304074d4ad7f05fe466f2cb476f54798096877ad3c5343692978776bd674b25261ecbeab87640f + languageName: node + linkType: hard + +"@types/serve-static@npm:*, @types/serve-static@npm:^2": + version: 2.2.0 + resolution: "@types/serve-static@npm:2.2.0" + dependencies: + "@types/http-errors": "*" + "@types/node": "*" + checksum: 0ad152ae2851cbe6c9381d0eca5fff8e6ff56afc0e03099efa88712c00318f52d8b01000be391375dcb2dbd912a54dacfc67ff36bae636a61570d74feba559b7 + languageName: node + linkType: hard + +"@types/serve-static@npm:^1": + version: 1.15.10 + resolution: "@types/serve-static@npm:1.15.10" + dependencies: + "@types/http-errors": "*" + "@types/node": "*" + "@types/send": <1 + checksum: f216eef2aaf2c8eff09f431c420c5c2989eaf0dfc15d106db9fb64c14577a4059af24fb0ae2eba7984d6360950c8cbc1fb52f65608106477729d251481bc96fe + languageName: node + linkType: hard + "@types/set-cookie-parser@npm:^2.4.0": version: 2.4.2 resolution: "@types/set-cookie-parser@npm:2.4.2" @@ -14817,6 +15236,16 @@ __metadata: languageName: node linkType: hard +"@types/swagger-ui-express@npm:^4.1.8": + version: 4.1.8 + resolution: "@types/swagger-ui-express@npm:4.1.8" + dependencies: + "@types/express": "*" + "@types/serve-static": "*" + checksum: bf4d84fb21c11e820238685cdc102f8d5e82d4382df9450a358bf80f70f5f0699c87645a291c52beee1eba5c0ee594e03714b236f1a0737272941a8708218f8f + languageName: node + linkType: hard + "@types/through@npm:*": version: 0.0.30 resolution: "@types/through@npm:0.0.30" @@ -16917,6 +17346,16 @@ __metadata: languageName: node linkType: hard +"accepts@npm:~1.3.8": + version: 1.3.8 + resolution: "accepts@npm:1.3.8" + dependencies: + mime-types: ~2.1.34 + negotiator: 0.6.3 + checksum: 50c43d32e7b50285ebe84b613ee4a3aa426715a7d131b65b786e2ead0fd76b6b60091b9916d3478a75f11f162628a2139991b6c03ab3f1d9ab7c86075dc8eab4 + languageName: node + linkType: hard + "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -17383,6 +17822,13 @@ __metadata: languageName: node linkType: hard +"array-flatten@npm:1.1.1": + version: 1.1.1 + resolution: "array-flatten@npm:1.1.1" + checksum: a9925bf3512d9dce202112965de90c222cd59a4fbfce68a0951d25d965cf44642931f40aac72309c41f12df19afa010ecadceb07cfff9ccc1621e99d89ab5f3b + languageName: node + linkType: hard + "array-ify@npm:^1.0.0": version: 1.0.0 resolution: "array-ify@npm:1.0.0" @@ -18708,6 +19154,26 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:~1.20.3": + version: 1.20.4 + resolution: "body-parser@npm:1.20.4" + dependencies: + bytes: ~3.1.2 + content-type: ~1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: ~1.2.0 + http-errors: ~2.0.1 + iconv-lite: ~0.4.24 + on-finished: ~2.4.1 + qs: ~6.14.0 + raw-body: ~2.5.3 + type-is: ~1.6.18 + unpipe: ~1.0.0 + checksum: eaa212cff1737d2fbb49fc7aa1d71d9b456adea2dc3de388ff3c6d67b28028d6b1fa7e6cd77e3670b4cbd402ab011f80f6e5bb811480b53a28d11f33678c6298 + languageName: node + linkType: hard + "borsh@npm:1.0.0": version: 1.0.0 resolution: "borsh@npm:1.0.0" @@ -19184,6 +19650,13 @@ __metadata: languageName: node linkType: hard +"bytes@npm:~3.1.2": + version: 3.1.2 + resolution: "bytes@npm:3.1.2" + checksum: e4bcd3948d289c5127591fbedf10c0b639ccbf00243504e4e127374a15c3bc8eed0d28d4aaab08ff6f1cf2abc0cce6ba3085ed32f4f90e82a5683ce0014e1b6e + languageName: node + linkType: hard + "cac@npm:^6.7.14": version: 6.7.14 resolution: "cac@npm:6.7.14" @@ -20038,6 +20511,22 @@ __metadata: languageName: node linkType: hard +"content-disposition@npm:~0.5.4": + version: 0.5.4 + resolution: "content-disposition@npm:0.5.4" + dependencies: + safe-buffer: 5.2.1 + checksum: afb9d545e296a5171d7574fcad634b2fdf698875f4006a9dd04a3e1333880c5c0c98d47b560d01216fb6505a54a2ba6a843ee3a02ec86d7e911e8315255f56c3 + languageName: node + linkType: hard + +"content-type@npm:~1.0.4, content-type@npm:~1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 566271e0a251642254cde0f845f9dd4f9856e52d988f4eb0d0dcffbb7a1f8ec98de7a5215fc628f3bce30fe2fb6fd2bc064b562d721658c59b544e2d34ea2766 + languageName: node + linkType: hard + "conventional-changelog-angular@npm:^5.0.11": version: 5.0.13 resolution: "conventional-changelog-angular@npm:5.0.13" @@ -20105,6 +20594,13 @@ __metadata: languageName: node linkType: hard +"cookie-signature@npm:~1.0.6": + version: 1.0.7 + resolution: "cookie-signature@npm:1.0.7" + checksum: 1a62808cd30d15fb43b70e19829b64d04b0802d8ef00275b57d152de4ae6a3208ca05c197b6668d104c4d9de389e53ccc2d3bc6bcaaffd9602461417d8c40710 + languageName: node + linkType: hard + "cookie@npm:^0.4.1": version: 0.4.1 resolution: "cookie@npm:0.4.1" @@ -20112,6 +20608,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:~0.7.1": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 9bf8555e33530affd571ea37b615ccad9b9a34febbf2c950c86787088eb00a8973690833b0f8ebd6b69b753c62669ea60cec89178c1fb007bf0749abed74f93e + languageName: node + linkType: hard + "cookiejar@npm:^2.1.1": version: 2.1.4 resolution: "cookiejar@npm:2.1.4" @@ -20828,6 +21331,15 @@ __metadata: languageName: node linkType: hard +"debug@npm:2.6.9, debug@npm:^2.2.0": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: 2.0.0 + checksum: d2f51589ca66df60bf36e1fa6e4386b318c3f1e06772280eea5b1ae9fd3d05e9c2b7fd8a7d862457d00853c75b00451aa2d7459b924629ee385287a650f58fe6 + languageName: node + linkType: hard + "debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": version: 4.3.4 resolution: "debug@npm:4.3.4" @@ -20840,15 +21352,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^2.2.0": - version: 2.6.9 - resolution: "debug@npm:2.6.9" - dependencies: - ms: 2.0.0 - checksum: d2f51589ca66df60bf36e1fa6e4386b318c3f1e06772280eea5b1ae9fd3d05e9c2b7fd8a7d862457d00853c75b00451aa2d7459b924629ee385287a650f58fe6 - languageName: node - linkType: hard - "debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" @@ -21077,7 +21580,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:2.0.0, depd@npm:^2.0.0": +"depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: abbe19c768c97ee2eed6282d8ce3031126662252c58d711f646921c9623f9052e3e1906443066beec1095832f534e57c523b7333f8e7e0d93051ab6baef5ab3a @@ -21124,6 +21627,13 @@ __metadata: languageName: node linkType: hard +"destroy@npm:1.2.0, destroy@npm:~1.2.0": + version: 1.2.0 + resolution: "destroy@npm:1.2.0" + checksum: 0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 + languageName: node + linkType: hard + "detect-browser@npm:5.2.0": version: 5.2.0 resolution: "detect-browser@npm:5.2.0" @@ -21456,6 +21966,13 @@ __metadata: languageName: node linkType: hard +"ee-first@npm:1.1.1": + version: 1.1.1 + resolution: "ee-first@npm:1.1.1" + checksum: 1b4cac778d64ce3b582a7e26b218afe07e207a0f9bfe13cc7395a6d307849cfe361e65033c3251e00c27dd060cab43014c2d6b2647676135e18b77d2d05b3f4f + languageName: node + linkType: hard + "eip-712@npm:^1.0.0": version: 1.0.0 resolution: "eip-712@npm:1.0.0" @@ -21648,6 +22165,13 @@ __metadata: languageName: node linkType: hard +"encodeurl@npm:~2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: abf5cd51b78082cf8af7be6785813c33b6df2068ce5191a40ca8b1afe6a86f9230af9a9ce694a5ce4665955e5c1120871826df9c128a642e09c58d592e2807fe + languageName: node + linkType: hard + "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -22485,6 +23009,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:~0.27.0": + version: 0.27.2 + resolution: "esbuild@npm:0.27.2" + dependencies: + "@esbuild/aix-ppc64": 0.27.2 + "@esbuild/android-arm": 0.27.2 + "@esbuild/android-arm64": 0.27.2 + "@esbuild/android-x64": 0.27.2 + "@esbuild/darwin-arm64": 0.27.2 + "@esbuild/darwin-x64": 0.27.2 + "@esbuild/freebsd-arm64": 0.27.2 + "@esbuild/freebsd-x64": 0.27.2 + "@esbuild/linux-arm": 0.27.2 + "@esbuild/linux-arm64": 0.27.2 + "@esbuild/linux-ia32": 0.27.2 + "@esbuild/linux-loong64": 0.27.2 + "@esbuild/linux-mips64el": 0.27.2 + "@esbuild/linux-ppc64": 0.27.2 + "@esbuild/linux-riscv64": 0.27.2 + "@esbuild/linux-s390x": 0.27.2 + "@esbuild/linux-x64": 0.27.2 + "@esbuild/netbsd-arm64": 0.27.2 + "@esbuild/netbsd-x64": 0.27.2 + "@esbuild/openbsd-arm64": 0.27.2 + "@esbuild/openbsd-x64": 0.27.2 + "@esbuild/openharmony-arm64": 0.27.2 + "@esbuild/sunos-x64": 0.27.2 + "@esbuild/win32-arm64": 0.27.2 + "@esbuild/win32-ia32": 0.27.2 + "@esbuild/win32-x64": 0.27.2 + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 62ec92f8f40ad19922ae7d8dbf0427e41744120a77cc95abdf099dfb484d65fbe3c70cc55b8eccb7f6cb0d14e871ff1f2f76376d476915c2a6d2b800269261b2 + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -22499,6 +23112,13 @@ __metadata: languageName: node linkType: hard +"escape-html@npm:~1.0.3": + version: 1.0.3 + resolution: "escape-html@npm:1.0.3" + checksum: 6213ca9ae00d0ab8bccb6d8d4e0a98e76237b2410302cf7df70aaa6591d509a2a37ce8998008cbecae8fc8ffaadf3fb0229535e6a145f3ce0b211d060decbb24 + languageName: node + linkType: hard + "escape-string-regexp@npm:2.0.0": version: 2.0.0 resolution: "escape-string-regexp@npm:2.0.0" @@ -22926,6 +23546,13 @@ __metadata: languageName: node linkType: hard +"etag@npm:~1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 571aeb3dbe0f2bbd4e4fadbdb44f325fc75335cd5f6f6b6a091e6a06a9f25ed5392f0863c5442acb0646787446e816f13cbfc6edce5b07658541dff573cab1ff + languageName: node + linkType: hard + "eth-block-tracker@npm:6.1.0": version: 6.1.0 resolution: "eth-block-tracker@npm:6.1.0" @@ -23566,6 +24193,45 @@ __metadata: languageName: node linkType: hard +"express@npm:^4.21.0": + version: 4.22.1 + resolution: "express@npm:4.22.1" + dependencies: + accepts: ~1.3.8 + array-flatten: 1.1.1 + body-parser: ~1.20.3 + content-disposition: ~0.5.4 + content-type: ~1.0.4 + cookie: ~0.7.1 + cookie-signature: ~1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: ~2.0.0 + escape-html: ~1.0.3 + etag: ~1.8.1 + finalhandler: ~1.3.1 + fresh: ~0.5.2 + http-errors: ~2.0.0 + merge-descriptors: 1.0.3 + methods: ~1.1.2 + on-finished: ~2.4.1 + parseurl: ~1.3.3 + path-to-regexp: ~0.1.12 + proxy-addr: ~2.0.7 + qs: ~6.14.0 + range-parser: ~1.2.1 + safe-buffer: 5.2.1 + send: ~0.19.0 + serve-static: ~1.16.2 + setprototypeof: 1.2.0 + statuses: ~2.0.1 + type-is: ~1.6.18 + utils-merge: 1.0.1 + vary: ~1.1.2 + checksum: 38fd76585f6a2394e02d499f852fc70c94c9b1527bd5812eb5ee45c23b7f1297baaf13c55162253b14c1e36939b8401429d6594095e63d01ca77447dac72894e + languageName: node + linkType: hard + "ext@npm:^1.1.2": version: 1.7.0 resolution: "ext@npm:1.7.0" @@ -23881,6 +24547,21 @@ __metadata: languageName: node linkType: hard +"finalhandler@npm:~1.3.1": + version: 1.3.2 + resolution: "finalhandler@npm:1.3.2" + dependencies: + debug: 2.6.9 + encodeurl: ~2.0.0 + escape-html: ~1.0.3 + on-finished: ~2.4.1 + parseurl: ~1.3.3 + statuses: ~2.0.2 + unpipe: ~1.0.0 + checksum: 4bce6b3e1f6998497a8ef8418bc307ef09daee05acc5a69a36da665565cbeb86218de1932e42dbf2eebf18f580053d2061eddbdeff9e312de45d46fbf4dd36ec + languageName: node + linkType: hard + "find-cache-dir@npm:^2.0.0": version: 2.1.0 resolution: "find-cache-dir@npm:2.1.0" @@ -24065,6 +24746,13 @@ __metadata: languageName: node linkType: hard +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: fd27e2394d8887ebd16a66ffc889dc983fbbd797d5d3f01087c020283c0f019a7d05ee85669383d8e0d216b116d720fc0cef2f6e9b7eb9f4c90c6e0bc7fd28e6 + languageName: node + linkType: hard + "framer-motion@npm:^12.6.5": version: 12.7.4 resolution: "framer-motion@npm:12.7.4" @@ -24096,6 +24784,13 @@ __metadata: languageName: node linkType: hard +"fresh@npm:~0.5.2": + version: 0.5.2 + resolution: "fresh@npm:0.5.2" + checksum: 13ea8b08f91e669a64e3ba3a20eb79d7ca5379a81f1ff7f4310d54e2320645503cc0c78daedc93dfb6191287295f6479544a649c64d8e41a1c0fb0c221552346 + languageName: node + linkType: hard + "friendly-challenge@npm:0.9.2": version: 0.9.2 resolution: "friendly-challenge@npm:0.9.2" @@ -25216,6 +25911,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:~2.0.0, http-errors@npm:~2.0.1": + version: 2.0.1 + resolution: "http-errors@npm:2.0.1" + dependencies: + depd: ~2.0.0 + inherits: ~2.0.4 + setprototypeof: ~1.2.0 + statuses: ~2.0.2 + toidentifier: ~1.0.1 + checksum: 155d1a100a06e4964597013109590b97540a177b69c3600bbc93efc746465a99a2b718f43cdf76b3791af994bbe3a5711002046bf668cdc007ea44cea6df7ccd + languageName: node + linkType: hard + "http-proxy-agent@npm:^5.0.0": version: 5.0.0 resolution: "http-proxy-agent@npm:5.0.0" @@ -25346,7 +26054,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.4.24": +"iconv-lite@npm:^0.4.24, iconv-lite@npm:~0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" dependencies: @@ -25657,6 +26365,13 @@ __metadata: languageName: node linkType: hard +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: f88d3825981486f5a1942414c8d77dd6674dd71c065adcfa46f578d677edcb99fda25af42675cb59db492fdf427b34a5abfcde3982da11a8fd83a500b41cfe77 + languageName: node + linkType: hard + "ipfs-only-hash@npm:^4.0.0": version: 4.0.0 resolution: "ipfs-only-hash@npm:4.0.0" @@ -27416,6 +28131,13 @@ __metadata: languageName: node linkType: hard +"lodash-es@npm:^4.17.21": + version: 4.17.22 + resolution: "lodash-es@npm:4.17.22" + checksum: 1da119f40e54822fb5d69eeb9d2c7ec46ac541cc73e6250748838abd7dd51ecf613592e07a5476f56bcbbeb27838697a4347d68cd98f131cad8c4665f9bb2a16 + languageName: node + linkType: hard + "lodash.clonedeep@npm:^4.5.0": version: 4.5.0 resolution: "lodash.clonedeep@npm:4.5.0" @@ -27742,6 +28464,13 @@ __metadata: languageName: node linkType: hard +"media-typer@npm:0.3.0": + version: 0.3.0 + resolution: "media-typer@npm:0.3.0" + checksum: af1b38516c28ec95d6b0826f6c8f276c58aec391f76be42aa07646b4e39d317723e869700933ca6995b056db4b09a78c92d5440dc23657e6764be5d28874bba1 + languageName: node + linkType: hard + "memdown@npm:^1.0.0": version: 1.4.1 resolution: "memdown@npm:1.4.1" @@ -27818,6 +28547,13 @@ __metadata: languageName: node linkType: hard +"merge-descriptors@npm:1.0.3": + version: 1.0.3 + resolution: "merge-descriptors@npm:1.0.3" + checksum: 52117adbe0313d5defa771c9993fe081e2d2df9b840597e966aadafde04ae8d0e3da46bac7ca4efc37d4d2b839436582659cd49c6a43eacb3fe3050896a105d1 + languageName: node + linkType: hard + "merge-options@npm:^3.0.4": version: 3.0.4 resolution: "merge-options@npm:3.0.4" @@ -27884,6 +28620,13 @@ __metadata: languageName: node linkType: hard +"methods@npm:~1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a + languageName: node + linkType: hard + "micro-ftch@npm:^0.3.1": version: 0.3.1 resolution: "micro-ftch@npm:0.3.1" @@ -27940,7 +28683,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.0.1, mime-types@npm:^2.1.12, mime-types@npm:^2.1.34, mime-types@npm:~2.1.19": +"mime-types@npm:^2.0.1, mime-types@npm:^2.1.12, mime-types@npm:^2.1.34, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -27949,6 +28692,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:1.6.0": + version: 1.6.0 + resolution: "mime@npm:1.6.0" + bin: + mime: cli.js + checksum: fef25e39263e6d207580bdc629f8872a3f9772c923c7f8c7e793175cee22777bbe8bba95e5d509a40aaa292d8974514ce634ae35769faa45f22d17edda5e8557 + languageName: node + linkType: hard + "mime@npm:^3.0.0": version: 3.0.0 resolution: "mime@npm:3.0.0" @@ -28319,7 +29071,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -28482,6 +29234,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:5.1.5": + version: 5.1.5 + resolution: "nanoid@npm:5.1.5" + bin: + nanoid: bin/nanoid.js + checksum: 6de2d006b51c983be385ef7ee285f7f2a57bd96f8c0ca881c4111461644bd81fafc2544f8e07cb834ca0f3e0f3f676c1fe78052183f008b0809efe6e273119f5 + languageName: node + linkType: hard + "nanoid@npm:^3.1.31, nanoid@npm:^3.3.6": version: 3.3.6 resolution: "nanoid@npm:3.3.6" @@ -28617,7 +29378,7 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^0.6.3": +"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 @@ -29232,6 +29993,15 @@ __metadata: languageName: node linkType: hard +"on-finished@npm:~2.4.1": + version: 2.4.1 + resolution: "on-finished@npm:2.4.1" + dependencies: + ee-first: 1.1.1 + checksum: d20929a25e7f0bb62f937a425b5edeb4e4cde0540d77ba146ec9357f00b0d497cdb3b9b05b9c8e46222407d1548d08166bff69cc56dfa55ba0e4469228920ff0 + languageName: node + linkType: hard + "once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -29292,6 +30062,15 @@ __metadata: languageName: node linkType: hard +"openapi3-ts@npm:^4.1.2": + version: 4.5.0 + resolution: "openapi3-ts@npm:4.5.0" + dependencies: + yaml: ^2.8.0 + checksum: 3a8609c8b00aed06dbc2fb8d203f86405ea3e45d7988432d2d0e4db7aa81b85ef3136d0acfaa7fcd47dadbf749d0e9f14d1b2f6ec79c9b2d174b02489f788aac + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.3 resolution: "optionator@npm:0.9.3" @@ -29683,6 +30462,13 @@ __metadata: languageName: node linkType: hard +"parseurl@npm:~1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2 + languageName: node + linkType: hard + "pascal-case@npm:^3.1.2": version: 3.1.2 resolution: "pascal-case@npm:3.1.2" @@ -29787,6 +30573,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:~0.1.12": + version: 0.1.12 + resolution: "path-to-regexp@npm:0.1.12" + checksum: ab237858bee7b25ecd885189f175ab5b5161e7b712b360d44f5c4516b8d271da3e4bf7bf0a7b9153ecb04c7d90ce8ff5158614e1208819cf62bac2b08452722e + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -30293,6 +31086,16 @@ __metadata: languageName: node linkType: hard +"proxy-addr@npm:~2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + checksum: 29c6990ce9364648255454842f06f8c46fcd124d3e6d7c5066df44662de63cdc0bad032e9bf5a3d653ff72141cc7b6019873d685708ac8210c30458ad99f2b74 + languageName: node + linkType: hard + "proxy-compare@npm:2.5.1": version: 2.5.1 resolution: "proxy-compare@npm:2.5.1" @@ -30504,7 +31307,7 @@ pvutils@latest: languageName: node linkType: hard -"qs@npm:^6.14.0, qs@npm:^6.14.1": +"qs@npm:^6.14.0, qs@npm:^6.14.1, qs@npm:~6.14.0": version: 6.14.1 resolution: "qs@npm:6.14.1" dependencies: @@ -30629,6 +31432,25 @@ pvutils@latest: languageName: node linkType: hard +"range-parser@npm:~1.2.1": + version: 1.2.1 + resolution: "range-parser@npm:1.2.1" + checksum: 0a268d4fea508661cf5743dfe3d5f47ce214fd6b7dec1de0da4d669dd4ef3d2144468ebe4179049eff253d9d27e719c88dae55be64f954e80135a0cada804ec9 + languageName: node + linkType: hard + +"raw-body@npm:~2.5.3": + version: 2.5.3 + resolution: "raw-body@npm:2.5.3" + dependencies: + bytes: ~3.1.2 + http-errors: ~2.0.1 + iconv-lite: ~0.4.24 + unpipe: ~1.0.0 + checksum: 16aa51e504318ebeef7f84a4d884c0f273cb0b7f3f14ea88788f92f5f488870617c97d4f886e84f119f21a2d6cdda3c4554821f8b18ed6be0d731ecb5a063d2a + languageName: node + linkType: hard + "re-reselect@npm:^4.0.0": version: 4.0.0 resolution: "re-reselect@npm:4.0.0" @@ -32355,7 +33177,7 @@ pvutils@latest: languageName: node linkType: hard -"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 @@ -32639,6 +33461,39 @@ pvutils@latest: languageName: node linkType: hard +"send@npm:~0.19.0, send@npm:~0.19.1": + version: 0.19.2 + resolution: "send@npm:0.19.2" + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: ~2.0.0 + escape-html: ~1.0.3 + etag: ~1.8.1 + fresh: ~0.5.2 + http-errors: ~2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: ~2.4.1 + range-parser: ~1.2.1 + statuses: ~2.0.2 + checksum: f9e11b718b48dbea72daa6a80e36e5a00fb6d01b1a6cfda8b3135c9ca9db84257738283da23371f437148ccd8f400e6171cd2a3642fb43fda462da407d9d30c0 + languageName: node + linkType: hard + +"serve-static@npm:~1.16.2": + version: 1.16.3 + resolution: "serve-static@npm:1.16.3" + dependencies: + encodeurl: ~2.0.0 + escape-html: ~1.0.3 + parseurl: ~1.3.3 + send: ~0.19.1 + checksum: ec7599540215e6676b223ea768bf7c256819180bf14f89d0b5d249a61bbb8f10b05b2a53048a153cb2cc7f3b367f1227d2fb715fe4b09d07299a9233eda1a453 + languageName: node + linkType: hard + "ses@npm:^0.18.7": version: 0.18.7 resolution: "ses@npm:0.18.7" @@ -32720,7 +33575,7 @@ pvutils@latest: languageName: node linkType: hard -"setprototypeof@npm:1.2.0": +"setprototypeof@npm:1.2.0, setprototypeof@npm:~1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" checksum: be18cbbf70e7d8097c97f713a2e76edf84e87299b40d085c6bf8b65314e994cc15e2e317727342fa6996e38e1f52c59720b53fe621e2eb593a6847bf0356db89 @@ -33292,6 +34147,13 @@ pvutils@latest: languageName: node linkType: hard +"statuses@npm:~2.0.1, statuses@npm:~2.0.2": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: 6927feb50c2a75b2a4caab2c565491f7a93ad3d8dbad7b1398d52359e9243a20e2ebe35e33726dee945125ef7a515e9097d8a1b910ba2bbd818265a2f6c39879 + languageName: node + linkType: hard + "std-env@npm:^3.4.3": version: 3.6.0 resolution: "std-env@npm:3.6.0" @@ -33782,6 +34644,13 @@ pvutils@latest: languageName: node linkType: hard +"tagged-tag@npm:^1.0.0": + version: 1.0.0 + resolution: "tagged-tag@npm:1.0.0" + checksum: e37653df3e495daa7ea7790cb161b810b00075bba2e4d6c93fb06a709e747e3ae9da11a120d0489833203926511b39e038a2affbd9d279cfb7a2f3fcccd30b5d + languageName: node + linkType: hard + "tar@npm:^6.1.11, tar@npm:^6.1.2": version: 6.1.13 resolution: "tar@npm:6.1.13" @@ -34115,7 +34984,7 @@ pvutils@latest: languageName: node linkType: hard -"toidentifier@npm:1.0.1": +"toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" checksum: 952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45 @@ -34429,6 +35298,22 @@ pvutils@latest: languageName: node linkType: hard +"tsx@npm:^4.19.2": + version: 4.21.0 + resolution: "tsx@npm:4.21.0" + dependencies: + esbuild: ~0.27.0 + fsevents: ~2.3.3 + get-tsconfig: ^4.7.5 + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: 50c98e4b6e66d1c30f72925c8e5e7be1a02377574de7cd367d7e7a6d4af43ca8ff659f91c654e7628b25a5498015e32f090529b92c679b0342811e1cf682e8cf + languageName: node + linkType: hard + "tty-browserify@npm:0.0.1": version: 0.0.1 resolution: "tty-browserify@npm:0.0.1" @@ -34489,6 +35374,15 @@ pvutils@latest: languageName: node linkType: hard +"type-fest@npm:5.0.0": + version: 5.0.0 + resolution: "type-fest@npm:5.0.0" + dependencies: + tagged-tag: ^1.0.0 + checksum: 2ae9bc0a5401b2f2061deb8f12c3ce175f8f6ad979c16b9f540fad40b447e035f149ff158e50dbcc0ca7e4d0ac6ba06659f9bf95e366f3dcda0079e76be08af9 + languageName: node + linkType: hard + "type-fest@npm:^0.18.0": version: 0.18.1 resolution: "type-fest@npm:0.18.1" @@ -34538,6 +35432,16 @@ pvutils@latest: languageName: node linkType: hard +"type-is@npm:~1.6.18": + version: 1.6.18 + resolution: "type-is@npm:1.6.18" + dependencies: + media-typer: 0.3.0 + mime-types: ~2.1.24 + checksum: 2c8e47675d55f8b4e404bcf529abdf5036c537a04c2b20177bcf78c9e3c1da69da3942b1346e6edb09e823228c0ee656ef0e033765ec39a70d496ef601a0c657 + languageName: node + linkType: hard + "type@npm:^1.0.1": version: 1.2.0 resolution: "type@npm:1.2.0" @@ -34751,6 +35655,16 @@ pvutils@latest: languageName: node linkType: hard +"typescript@npm:^5.7.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 0d0ffb84f2cd072c3e164c79a2e5a1a1f4f168e84cb2882ff8967b92afe1def6c2a91f6838fb58b168428f9458c57a2ba06a6737711fdd87a256bbe83e9a217f + languageName: node + linkType: hard + "typescript@patch:typescript@<4.8.0#~builtin, typescript@patch:typescript@^4.4.3#~builtin": version: 4.7.4 resolution: "typescript@patch:typescript@npm%3A4.7.4#~builtin::version=4.7.4&hash=65a307" @@ -34781,6 +35695,16 @@ pvutils@latest: languageName: node linkType: hard +"typescript@patch:typescript@^5.7.3#~builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#~builtin::version=5.9.3&hash=85af82" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 8bb8d86819ac86a498eada254cad7fb69c5f74778506c700c2a712daeaff21d3a6f51fd0d534fe16903cb010d1b74f89437a3d02d4d0ff5ca2ba9a4660de8497 + languageName: node + linkType: hard + "ua-is-frozen@npm:^0.1.2": version: 0.1.2 resolution: "ua-is-frozen@npm:0.1.2" @@ -35028,6 +35952,13 @@ pvutils@latest: languageName: node linkType: hard +"unpipe@npm:~1.0.0": + version: 1.0.0 + resolution: "unpipe@npm:1.0.0" + checksum: 4fa18d8d8d977c55cb09715385c203197105e10a6d220087ec819f50cb68870f02942244f1017565484237f1f8c5d3cd413631b1ae104d3096f24fdfde1b4aa2 + languageName: node + linkType: hard + "unplugin@npm:2.1.0": version: 2.1.0 resolution: "unplugin@npm:2.1.0" @@ -35369,6 +36300,13 @@ pvutils@latest: languageName: node linkType: hard +"utils-merge@npm:1.0.1": + version: 1.0.1 + resolution: "utils-merge@npm:1.0.1" + checksum: c81095493225ecfc28add49c106ca4f09cdf56bc66731aa8dabc2edbbccb1e1bfe2de6a115e5c6a380d3ea166d1636410b62ef216bb07b3feb1cfde1d95d5080 + languageName: node + linkType: hard + "uuid@npm:8.3.2, uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" @@ -35570,7 +36508,7 @@ pvutils@latest: languageName: node linkType: hard -"vary@npm:^1": +"vary@npm:^1, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b @@ -36821,6 +37759,15 @@ pvutils@latest: languageName: node linkType: hard +"yaml@npm:^2.8.0, yaml@npm:^2.8.2": + version: 2.8.2 + resolution: "yaml@npm:2.8.2" + bin: + yaml: bin.mjs + checksum: 5ffd9f23bc7a450129cbd49dcf91418988f154ede10c83fd28ab293661ac2783c05da19a28d76a22cbd77828eae25d4bd7453f9a9fe2d287d085d72db46fd105 + languageName: node + linkType: hard + "yamljs@npm:^0.3.0": version: 0.3.0 resolution: "yamljs@npm:0.3.0" @@ -36993,6 +37940,13 @@ pvutils@latest: languageName: node linkType: hard +"zod@npm:3.23.8": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 15949ff82118f59c893dacd9d3c766d02b6fa2e71cf474d5aa888570c469dbf5446ac5ad562bb035bf7ac9650da94f290655c194f4a6de3e766f43febd432c5c + languageName: node + linkType: hard + "zod@npm:^3.23.8": version: 3.25.76 resolution: "zod@npm:3.25.76" @@ -37000,6 +37954,13 @@ pvutils@latest: languageName: node linkType: hard +"zod@npm:^4.1.11": + version: 4.3.5 + resolution: "zod@npm:4.3.5" + checksum: 68691183a91c67c4102db20139f3b5af288c59b4b11eb2239d712aae99dc6c1cecaeebcb0c012b44489771be05fecba21e79f65af4b3163b220239ef0af3ec49 + languageName: node + linkType: hard + "zod@npm:^4.2.1": version: 4.2.1 resolution: "zod@npm:4.2.1"