@sip-protocol/api 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/routes/index.js +1164 -145
- package/dist/server.d.ts +29 -1
- package/dist/server.js +1514 -173
- package/package.json +25 -11
- package/src/config.ts +121 -0
- package/src/logger.ts +99 -0
- package/src/middleware/auth.ts +128 -0
- package/src/middleware/cors.ts +163 -0
- package/src/middleware/error-handler.ts +18 -7
- package/src/middleware/index.ts +13 -1
- package/src/middleware/rate-limit.ts +203 -0
- package/src/middleware/request-id.ts +66 -0
- package/src/middleware/validation.ts +100 -11
- package/src/monitoring/index.ts +36 -0
- package/src/monitoring/metrics.ts +163 -0
- package/src/monitoring/sentry.ts +179 -0
- package/src/routes/commitment.ts +1 -1
- package/src/routes/health.ts +35 -3
- package/src/routes/index.ts +2 -0
- package/src/routes/metrics.ts +27 -0
- package/src/routes/proof.ts +68 -1
- package/src/routes/swap.ts +116 -47
- package/src/routes/webhook.ts +156 -0
- package/src/server.ts +142 -19
- package/src/services/helius-listener.ts +104 -0
- package/src/services/index.ts +15 -0
- package/src/services/token-metadata.ts +91 -0
- package/src/services/webhook-delivery.ts +146 -0
- package/src/shutdown.ts +119 -0
- package/src/stores/index.ts +2 -0
- package/src/stores/swap-store.ts +158 -0
- package/src/stores/webhook-store.ts +120 -0
- package/src/types/api.ts +67 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentry Error Monitoring
|
|
3
|
+
*
|
|
4
|
+
* Integrates Sentry for production error tracking:
|
|
5
|
+
* - Captures unhandled exceptions
|
|
6
|
+
* - Captures unhandled promise rejections
|
|
7
|
+
* - Provides stack traces with source maps
|
|
8
|
+
* - Enriches errors with request context
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as Sentry from '@sentry/node'
|
|
12
|
+
import type { Express, ErrorRequestHandler } from 'express'
|
|
13
|
+
import { env } from '../config'
|
|
14
|
+
|
|
15
|
+
let isInitialized = false
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize Sentry error monitoring
|
|
19
|
+
*
|
|
20
|
+
* Only initializes if SENTRY_DSN is provided
|
|
21
|
+
*/
|
|
22
|
+
export function initSentry(): void {
|
|
23
|
+
if (isInitialized) return
|
|
24
|
+
|
|
25
|
+
if (!env.SENTRY_DSN) {
|
|
26
|
+
if (env.isProduction) {
|
|
27
|
+
console.warn('[Sentry] SENTRY_DSN not set - error monitoring disabled')
|
|
28
|
+
}
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Sentry.init({
|
|
33
|
+
dsn: env.SENTRY_DSN,
|
|
34
|
+
environment: env.NODE_ENV,
|
|
35
|
+
release: `sip-api@${process.env.npm_package_version || '0.1.0'}`,
|
|
36
|
+
|
|
37
|
+
// Performance monitoring
|
|
38
|
+
tracesSampleRate: env.isProduction ? 0.1 : 1.0,
|
|
39
|
+
|
|
40
|
+
// Filter out noisy errors
|
|
41
|
+
ignoreErrors: [
|
|
42
|
+
// Expected client errors
|
|
43
|
+
'Request validation failed',
|
|
44
|
+
'Unauthorized',
|
|
45
|
+
// Network issues
|
|
46
|
+
'ECONNRESET',
|
|
47
|
+
'EPIPE',
|
|
48
|
+
],
|
|
49
|
+
|
|
50
|
+
// Add extra context
|
|
51
|
+
beforeSend(event) {
|
|
52
|
+
// Don't send events in test environment
|
|
53
|
+
if (env.NODE_ENV === 'test') {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add request ID tag for correlation
|
|
58
|
+
const requestId = event.request?.headers?.['x-request-id']
|
|
59
|
+
if (requestId && typeof requestId === 'string') {
|
|
60
|
+
event.tags = { ...event.tags, requestId }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Sanitize sensitive data
|
|
64
|
+
if (event.request?.headers) {
|
|
65
|
+
delete event.request.headers['authorization']
|
|
66
|
+
delete event.request.headers['x-api-key']
|
|
67
|
+
delete event.request.headers['cookie']
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return event
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Capture breadcrumbs for debugging
|
|
74
|
+
beforeBreadcrumb(breadcrumb) {
|
|
75
|
+
// Filter out health check noise
|
|
76
|
+
if (breadcrumb.category === 'http' && breadcrumb.data?.url?.includes('/health')) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
return breadcrumb
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
isInitialized = true
|
|
84
|
+
console.log('[Sentry] Initialized for environment:', env.NODE_ENV)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if Sentry is initialized
|
|
89
|
+
*/
|
|
90
|
+
export function isSentryEnabled(): boolean {
|
|
91
|
+
return isInitialized && !!env.SENTRY_DSN
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Capture an exception with optional context
|
|
96
|
+
*/
|
|
97
|
+
export function captureException(
|
|
98
|
+
error: Error,
|
|
99
|
+
context?: Record<string, unknown>
|
|
100
|
+
): string | undefined {
|
|
101
|
+
if (!isSentryEnabled()) return undefined
|
|
102
|
+
|
|
103
|
+
return Sentry.captureException(error, {
|
|
104
|
+
extra: context,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Capture a message with severity level
|
|
110
|
+
*/
|
|
111
|
+
export function captureMessage(
|
|
112
|
+
message: string,
|
|
113
|
+
level: Sentry.SeverityLevel = 'info'
|
|
114
|
+
): string | undefined {
|
|
115
|
+
if (!isSentryEnabled()) return undefined
|
|
116
|
+
|
|
117
|
+
return Sentry.captureMessage(message, level)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Set user context for error tracking
|
|
122
|
+
*/
|
|
123
|
+
export function setUser(user: { id: string; [key: string]: unknown } | null): void {
|
|
124
|
+
if (!isSentryEnabled()) return
|
|
125
|
+
|
|
126
|
+
Sentry.setUser(user)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Add context tags
|
|
131
|
+
*/
|
|
132
|
+
export function setTags(tags: Record<string, string>): void {
|
|
133
|
+
if (!isSentryEnabled()) return
|
|
134
|
+
|
|
135
|
+
Sentry.setTags(tags)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Setup Express error handler for Sentry
|
|
140
|
+
* Call after all routes are registered
|
|
141
|
+
*/
|
|
142
|
+
export function setupSentryErrorHandler(app: Express): void {
|
|
143
|
+
if (!isSentryEnabled()) return
|
|
144
|
+
|
|
145
|
+
Sentry.setupExpressErrorHandler(app)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a no-op request handler middleware
|
|
150
|
+
* Sentry v10 uses auto-instrumentation, but we keep this for compatibility
|
|
151
|
+
*/
|
|
152
|
+
export function sentryRequestHandler(
|
|
153
|
+
_req: unknown,
|
|
154
|
+
_res: unknown,
|
|
155
|
+
next: () => void
|
|
156
|
+
): void {
|
|
157
|
+
next()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create a no-op error handler for compatibility
|
|
162
|
+
* Actual error handling is done via setupSentryErrorHandler
|
|
163
|
+
*/
|
|
164
|
+
export const sentryErrorHandler: ErrorRequestHandler = (err, _req, _res, next) => {
|
|
165
|
+
// Sentry v10 captures errors automatically via setupExpressErrorHandler
|
|
166
|
+
// This middleware is kept for API compatibility
|
|
167
|
+
next(err)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Flush pending events (call before shutdown)
|
|
172
|
+
*/
|
|
173
|
+
export async function flushSentry(timeout = 2000): Promise<boolean> {
|
|
174
|
+
if (!isSentryEnabled()) return true
|
|
175
|
+
|
|
176
|
+
return Sentry.close(timeout)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { Sentry }
|
package/src/routes/commitment.ts
CHANGED
|
@@ -45,7 +45,7 @@ router.post(
|
|
|
45
45
|
const { value, blindingFactor } = req.body as CreateCommitmentRequest
|
|
46
46
|
|
|
47
47
|
const valueBigInt = BigInt(value)
|
|
48
|
-
const blindingBytes = blindingFactor ? hexToBytes(blindingFactor) : undefined
|
|
48
|
+
const blindingBytes = blindingFactor ? hexToBytes(blindingFactor.replace(/^0x/, '')) : undefined
|
|
49
49
|
|
|
50
50
|
const result = commit(valueBigInt, blindingBytes)
|
|
51
51
|
|
package/src/routes/health.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Router, Request, Response } from 'express'
|
|
2
2
|
import type { HealthResponse, ApiResponse } from '../types/api'
|
|
3
|
+
import { isServerShuttingDown } from '../shutdown'
|
|
4
|
+
import { swapStore } from '../stores'
|
|
5
|
+
import { isProofProviderReady, getProofInitError } from './proof'
|
|
6
|
+
import { getRedisStatus } from '../middleware/rate-limit'
|
|
3
7
|
|
|
4
8
|
const router: Router = Router()
|
|
5
9
|
|
|
@@ -38,17 +42,45 @@ const startTime = Date.now()
|
|
|
38
42
|
* type: number
|
|
39
43
|
*/
|
|
40
44
|
router.get('/', (req: Request, res: Response) => {
|
|
45
|
+
const shuttingDown = isServerShuttingDown()
|
|
46
|
+
const status = shuttingDown ? 'shutting_down' : 'healthy'
|
|
47
|
+
const statusCode = shuttingDown ? 503 : 200
|
|
48
|
+
|
|
49
|
+
const cacheMetrics = swapStore.getMetrics()
|
|
50
|
+
|
|
51
|
+
const proofReady = isProofProviderReady()
|
|
52
|
+
const proofError = getProofInitError()
|
|
53
|
+
const redisStatus = getRedisStatus()
|
|
54
|
+
|
|
41
55
|
const response: ApiResponse<HealthResponse> = {
|
|
42
|
-
success:
|
|
56
|
+
success: !shuttingDown,
|
|
43
57
|
data: {
|
|
44
|
-
status: 'healthy',
|
|
58
|
+
status: status as 'healthy',
|
|
45
59
|
version: process.env.npm_package_version || '0.1.0',
|
|
46
60
|
timestamp: new Date().toISOString(),
|
|
47
61
|
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
62
|
+
services: {
|
|
63
|
+
proofProvider: {
|
|
64
|
+
ready: proofReady,
|
|
65
|
+
error: proofError?.message || null,
|
|
66
|
+
},
|
|
67
|
+
rateLimiter: {
|
|
68
|
+
store: redisStatus.storeType,
|
|
69
|
+
redisConfigured: redisStatus.configured,
|
|
70
|
+
redisConnected: redisStatus.connected,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
cache: {
|
|
74
|
+
swaps: {
|
|
75
|
+
size: cacheMetrics.size,
|
|
76
|
+
maxSize: cacheMetrics.maxSize,
|
|
77
|
+
utilizationPercent: cacheMetrics.utilizationPercent,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
48
80
|
},
|
|
49
81
|
}
|
|
50
82
|
|
|
51
|
-
res.json(response)
|
|
83
|
+
res.status(statusCode).json(response)
|
|
52
84
|
})
|
|
53
85
|
|
|
54
86
|
export default router
|
package/src/routes/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import stealthRouter from './stealth'
|
|
|
4
4
|
import commitmentRouter from './commitment'
|
|
5
5
|
import proofRouter from './proof'
|
|
6
6
|
import swapRouter from './swap'
|
|
7
|
+
import webhookRouter from './webhook'
|
|
7
8
|
|
|
8
9
|
const router: Router = Router()
|
|
9
10
|
|
|
@@ -14,5 +15,6 @@ router.use('/commitment', commitmentRouter)
|
|
|
14
15
|
router.use('/proof', proofRouter)
|
|
15
16
|
router.use('/quote', swapRouter) // POST /quote
|
|
16
17
|
router.use('/swap', swapRouter) // POST /swap and GET /swap/:id/status
|
|
18
|
+
router.use('/webhooks', webhookRouter) // Webhook registration CRUD + Helius ingest
|
|
17
19
|
|
|
18
20
|
export default router
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prometheus Metrics Endpoint
|
|
3
|
+
*
|
|
4
|
+
* GET /metrics - Prometheus-compatible metrics
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Router, Request, Response } from 'express'
|
|
8
|
+
import { register } from '../monitoring'
|
|
9
|
+
|
|
10
|
+
const router: Router = Router()
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GET /metrics
|
|
14
|
+
* Prometheus metrics endpoint
|
|
15
|
+
*
|
|
16
|
+
* Returns metrics in Prometheus text format for scraping
|
|
17
|
+
*/
|
|
18
|
+
router.get('/', async (req: Request, res: Response) => {
|
|
19
|
+
try {
|
|
20
|
+
res.set('Content-Type', register.contentType)
|
|
21
|
+
res.end(await register.metrics())
|
|
22
|
+
} catch (err) {
|
|
23
|
+
res.status(500).end()
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export default router
|
package/src/routes/proof.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Router, Request, Response } from 'express'
|
|
|
2
2
|
import { MockProofProvider } from '@sip-protocol/sdk'
|
|
3
3
|
import { hexToBytes } from '@noble/hashes/utils'
|
|
4
4
|
import { validateRequest, schemas } from '../middleware'
|
|
5
|
+
import { logger } from '../logger'
|
|
5
6
|
import type { GenerateFundingProofRequest, FundingProofResponse, ApiResponse } from '../types/api'
|
|
6
7
|
|
|
7
8
|
const router: Router = Router()
|
|
@@ -10,6 +11,58 @@ const router: Router = Router()
|
|
|
10
11
|
// In production, use NoirProofProvider from '@sip-protocol/sdk/proofs/noir'
|
|
11
12
|
const proofProvider = new MockProofProvider()
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Proof provider initialization state
|
|
16
|
+
* Implements fail-fast pattern with retry for transient failures
|
|
17
|
+
*/
|
|
18
|
+
let proofProviderReady = false
|
|
19
|
+
let proofInitError: Error | null = null
|
|
20
|
+
|
|
21
|
+
const MAX_INIT_RETRIES = 3
|
|
22
|
+
const RETRY_DELAY_MS = 2000
|
|
23
|
+
|
|
24
|
+
async function initializeProofProvider(): Promise<void> {
|
|
25
|
+
for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
|
|
26
|
+
try {
|
|
27
|
+
await proofProvider.initialize()
|
|
28
|
+
proofProviderReady = true
|
|
29
|
+
proofInitError = null
|
|
30
|
+
logger.info({ attempt }, 'Proof provider initialized successfully')
|
|
31
|
+
return
|
|
32
|
+
} catch (err) {
|
|
33
|
+
proofInitError = err instanceof Error ? err : new Error(String(err))
|
|
34
|
+
logger.warn({ attempt, maxRetries: MAX_INIT_RETRIES, error: proofInitError.message },
|
|
35
|
+
'Proof provider initialization failed, retrying...')
|
|
36
|
+
|
|
37
|
+
if (attempt < MAX_INIT_RETRIES) {
|
|
38
|
+
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * attempt))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// All retries exhausted - log fatal but don't crash (graceful degradation)
|
|
44
|
+
// The readiness guard will reject proof requests until fixed
|
|
45
|
+
logger.error({ error: proofInitError?.message },
|
|
46
|
+
'Proof provider initialization failed after all retries')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Start initialization immediately (non-blocking)
|
|
50
|
+
initializeProofProvider()
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if proof provider is ready
|
|
54
|
+
*/
|
|
55
|
+
export function isProofProviderReady(): boolean {
|
|
56
|
+
return proofProviderReady
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get proof provider initialization error (if any)
|
|
61
|
+
*/
|
|
62
|
+
export function getProofInitError(): Error | null {
|
|
63
|
+
return proofInitError
|
|
64
|
+
}
|
|
65
|
+
|
|
13
66
|
/**
|
|
14
67
|
* POST /proof/funding
|
|
15
68
|
* Generate a funding proof (proves balance >= minimum without revealing exact balance)
|
|
@@ -52,11 +105,25 @@ router.post(
|
|
|
52
105
|
'/funding',
|
|
53
106
|
validateRequest({ body: schemas.generateFundingProof }),
|
|
54
107
|
async (req: Request, res: Response) => {
|
|
108
|
+
// Readiness guard - reject requests if proof provider not initialized
|
|
109
|
+
if (!proofProviderReady) {
|
|
110
|
+
const errorMsg = proofInitError?.message || 'Proof provider is initializing'
|
|
111
|
+
logger.warn({ error: errorMsg }, 'Proof request rejected - provider not ready')
|
|
112
|
+
return res.status(503).json({
|
|
113
|
+
success: false,
|
|
114
|
+
error: {
|
|
115
|
+
code: 'PROOF_PROVIDER_NOT_READY',
|
|
116
|
+
message: 'Proof generation service is not ready',
|
|
117
|
+
details: { reason: errorMsg },
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
55
122
|
const { balance, minRequired, balanceBlinding } = req.body as GenerateFundingProofRequest
|
|
56
123
|
|
|
57
124
|
const balanceBigInt = BigInt(balance)
|
|
58
125
|
const minRequiredBigInt = BigInt(minRequired)
|
|
59
|
-
const balanceBlindingBytes = hexToBytes(balanceBlinding)
|
|
126
|
+
const balanceBlindingBytes = hexToBytes(balanceBlinding.replace(/^0x/, ''))
|
|
60
127
|
|
|
61
128
|
const result = await proofProvider.generateFundingProof({
|
|
62
129
|
balance: balanceBigInt,
|
package/src/routes/swap.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Router, Request, Response } from 'express'
|
|
2
|
-
import { SIP, PrivacyLevel } from '@sip-protocol/sdk'
|
|
3
|
-
import { validateRequest, schemas } from '../middleware'
|
|
2
|
+
import { SIP, PrivacyLevel, getAsset, isKnownToken } from '@sip-protocol/sdk'
|
|
3
|
+
import { validateRequest, schemas, calculateMinAmount, percentToBps } from '../middleware'
|
|
4
|
+
import { swapStore } from '../stores'
|
|
5
|
+
import { env } from '../config'
|
|
6
|
+
import { logger } from '../logger'
|
|
4
7
|
import type {
|
|
5
8
|
GetQuoteRequest,
|
|
6
9
|
ExecuteSwapRequest,
|
|
@@ -15,19 +18,15 @@ const router: Router = Router()
|
|
|
15
18
|
// Initialize SIP client
|
|
16
19
|
const sip = new SIP({ network: 'testnet' })
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Check if mock mode is allowed
|
|
23
|
+
* In production, mock mode should be explicitly disabled
|
|
24
|
+
*/
|
|
25
|
+
const MOCK_MODE_ENABLED = env.NODE_ENV !== 'production'
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
status: 'pending' | 'processing' | 'completed' | 'failed'
|
|
24
|
-
transactionHash?: HexString
|
|
25
|
-
inputAmount: string
|
|
26
|
-
outputAmount?: string
|
|
27
|
-
createdAt: string
|
|
28
|
-
updatedAt: string
|
|
29
|
-
error?: string
|
|
30
|
-
}>()
|
|
27
|
+
if (!MOCK_MODE_ENABLED) {
|
|
28
|
+
logger.warn('Production mode: Mock quotes and swaps are DISABLED')
|
|
29
|
+
}
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
32
|
* POST /quote
|
|
@@ -82,36 +81,78 @@ router.post(
|
|
|
82
81
|
slippageTolerance
|
|
83
82
|
} = req.body as GetQuoteRequest
|
|
84
83
|
|
|
85
|
-
//
|
|
84
|
+
// PRODUCTION MODE: Fail if quote aggregator not configured
|
|
85
|
+
if (!MOCK_MODE_ENABLED) {
|
|
86
|
+
return res.status(503).json({
|
|
87
|
+
success: false,
|
|
88
|
+
error: {
|
|
89
|
+
code: 'QUOTE_SERVICE_UNAVAILABLE',
|
|
90
|
+
message: 'Real quote aggregator not configured. This API is not ready for production use.',
|
|
91
|
+
details: {
|
|
92
|
+
hint: 'Configure QUOTE_AGGREGATOR_URL or use development mode',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Validate tokens are known (fail fast on unknown tokens)
|
|
99
|
+
if (!isKnownToken(inputToken, inputChain)) {
|
|
100
|
+
return res.status(400).json({
|
|
101
|
+
success: false,
|
|
102
|
+
error: {
|
|
103
|
+
code: 'UNKNOWN_TOKEN',
|
|
104
|
+
message: `Unknown input token: ${inputToken} on ${inputChain}`,
|
|
105
|
+
},
|
|
106
|
+
} satisfies ApiResponse<never>)
|
|
107
|
+
}
|
|
108
|
+
if (!isKnownToken(outputToken, outputChain)) {
|
|
109
|
+
return res.status(400).json({
|
|
110
|
+
success: false,
|
|
111
|
+
error: {
|
|
112
|
+
code: 'UNKNOWN_TOKEN',
|
|
113
|
+
message: `Unknown output token: ${outputToken} on ${outputChain}`,
|
|
114
|
+
},
|
|
115
|
+
} satisfies ApiResponse<never>)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Get asset info with correct decimals from registry
|
|
119
|
+
const inputAsset = getAsset(inputToken, inputChain)
|
|
120
|
+
const outputAsset = getAsset(outputToken, outputChain)
|
|
121
|
+
|
|
122
|
+
// Parse and validate amount (already validated by schema, safe to parse)
|
|
123
|
+
const inputAmountBigInt = BigInt(inputAmount)
|
|
124
|
+
const slippagePercent = slippageTolerance ?? 1
|
|
125
|
+
const slippageBps = percentToBps(slippagePercent)
|
|
126
|
+
|
|
127
|
+
// Create intent with safe slippage calculation
|
|
86
128
|
const intent = await sip.createIntent({
|
|
87
129
|
input: {
|
|
88
|
-
asset:
|
|
89
|
-
|
|
90
|
-
address: null, // Native token
|
|
91
|
-
symbol: inputToken,
|
|
92
|
-
decimals: 9,
|
|
93
|
-
},
|
|
94
|
-
amount: BigInt(inputAmount),
|
|
130
|
+
asset: inputAsset,
|
|
131
|
+
amount: inputAmountBigInt,
|
|
95
132
|
},
|
|
96
133
|
output: {
|
|
97
|
-
asset:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
symbol: outputToken,
|
|
101
|
-
decimals: 9,
|
|
102
|
-
},
|
|
103
|
-
minAmount: BigInt(inputAmount) * 95n / 100n, // 5% slippage
|
|
104
|
-
maxSlippage: (slippageTolerance || 1) / 100,
|
|
134
|
+
asset: outputAsset,
|
|
135
|
+
minAmount: calculateMinAmount(inputAmountBigInt, slippageBps),
|
|
136
|
+
maxSlippage: slippagePercent / 100,
|
|
105
137
|
},
|
|
106
138
|
privacy: PrivacyLevel.TRANSPARENT, // Default to transparent for quote
|
|
107
139
|
})
|
|
108
140
|
|
|
109
|
-
//
|
|
110
|
-
// In production, this would
|
|
141
|
+
// DEV MODE: Return mock quote with warning
|
|
142
|
+
// In production, this block would never execute (see check above)
|
|
143
|
+
logger.warn({
|
|
144
|
+
inputChain,
|
|
145
|
+
inputToken,
|
|
146
|
+
outputChain,
|
|
147
|
+
outputToken,
|
|
148
|
+
inputAmount,
|
|
149
|
+
}, 'Returning MOCK quote - not for production use')
|
|
150
|
+
|
|
111
151
|
const mockQuote: QuoteResponse = {
|
|
112
152
|
quoteId: `quote-${Date.now()}`,
|
|
113
153
|
inputAmount,
|
|
114
|
-
|
|
154
|
+
// Mock: 5% fee (clearly fake rate)
|
|
155
|
+
outputAmount: (BigInt(inputAmount) * 95n / 100n).toString(),
|
|
115
156
|
rate: '0.95',
|
|
116
157
|
estimatedTime: 30,
|
|
117
158
|
fees: {
|
|
@@ -130,9 +171,12 @@ router.post(
|
|
|
130
171
|
},
|
|
131
172
|
}
|
|
132
173
|
|
|
133
|
-
const response: ApiResponse<QuoteResponse> = {
|
|
174
|
+
const response: ApiResponse<QuoteResponse & { _warning?: string }> = {
|
|
134
175
|
success: true,
|
|
135
|
-
data:
|
|
176
|
+
data: {
|
|
177
|
+
...mockQuote,
|
|
178
|
+
_warning: 'MOCK_DATA: This quote uses simulated pricing. Do not use for real transactions.',
|
|
179
|
+
},
|
|
136
180
|
}
|
|
137
181
|
|
|
138
182
|
res.json(response)
|
|
@@ -176,33 +220,56 @@ router.post(
|
|
|
176
220
|
'/swap',
|
|
177
221
|
validateRequest({ body: schemas.executeSwap }),
|
|
178
222
|
async (req: Request, res: Response) => {
|
|
179
|
-
const { intentId, quoteId,
|
|
223
|
+
const { intentId, quoteId, inputAmount } = req.body as ExecuteSwapRequest & { inputAmount?: string }
|
|
224
|
+
|
|
225
|
+
// PRODUCTION MODE: Fail if swap executor not configured
|
|
226
|
+
if (!MOCK_MODE_ENABLED) {
|
|
227
|
+
return res.status(503).json({
|
|
228
|
+
success: false,
|
|
229
|
+
error: {
|
|
230
|
+
code: 'SWAP_SERVICE_UNAVAILABLE',
|
|
231
|
+
message: 'Real swap executor not configured. This API is not ready for production use.',
|
|
232
|
+
details: {
|
|
233
|
+
hint: 'Configure SWAP_EXECUTOR_URL or use development mode',
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
})
|
|
237
|
+
}
|
|
180
238
|
|
|
181
239
|
// Generate swap ID
|
|
182
240
|
const swapId = `swap-${Date.now()}`
|
|
183
241
|
|
|
184
|
-
//
|
|
242
|
+
// Track actual input amount from request (not hardcoded)
|
|
243
|
+
// In dev mode, allow a default for testing but warn about it
|
|
244
|
+
const actualInputAmount = inputAmount || '0'
|
|
245
|
+
if (!inputAmount) {
|
|
246
|
+
logger.warn({ swapId, quoteId, intentId }, 'Swap created without inputAmount - using 0')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Store swap in LRU cache with TTL
|
|
185
250
|
const swap = {
|
|
186
251
|
id: swapId,
|
|
187
252
|
status: 'pending' as const,
|
|
188
|
-
inputAmount:
|
|
253
|
+
inputAmount: actualInputAmount,
|
|
189
254
|
createdAt: new Date().toISOString(),
|
|
190
255
|
updatedAt: new Date().toISOString(),
|
|
191
256
|
}
|
|
192
|
-
|
|
257
|
+
swapStore.set(swapId, swap)
|
|
193
258
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
259
|
+
logger.warn({
|
|
260
|
+
swapId,
|
|
261
|
+
quoteId,
|
|
262
|
+
intentId,
|
|
263
|
+
inputAmount: actualInputAmount,
|
|
264
|
+
}, 'Creating MOCK swap - not for production use')
|
|
199
265
|
|
|
200
|
-
const response: ApiResponse<SwapResponse> = {
|
|
266
|
+
const response: ApiResponse<SwapResponse & { _warning?: string }> = {
|
|
201
267
|
success: true,
|
|
202
268
|
data: {
|
|
203
269
|
swapId,
|
|
204
270
|
status: 'pending',
|
|
205
271
|
timestamp: new Date().toISOString(),
|
|
272
|
+
_warning: 'MOCK_DATA: This swap is simulated. No real transaction will be executed.',
|
|
206
273
|
},
|
|
207
274
|
}
|
|
208
275
|
|
|
@@ -237,15 +304,17 @@ router.get(
|
|
|
237
304
|
validateRequest({ params: schemas.swapStatus }),
|
|
238
305
|
async (req: Request, res: Response) => {
|
|
239
306
|
const { id } = req.params
|
|
307
|
+
// Express 5 types params as string | string[] - ensure we have a string
|
|
308
|
+
const swapId = Array.isArray(id) ? id[0] : id
|
|
240
309
|
|
|
241
|
-
const swap =
|
|
310
|
+
const swap = swapStore.get(swapId)
|
|
242
311
|
|
|
243
312
|
if (!swap) {
|
|
244
313
|
return res.status(404).json({
|
|
245
314
|
success: false,
|
|
246
315
|
error: {
|
|
247
316
|
code: 'SWAP_NOT_FOUND',
|
|
248
|
-
message: `Swap ${
|
|
317
|
+
message: `Swap ${swapId} not found`,
|
|
249
318
|
},
|
|
250
319
|
})
|
|
251
320
|
}
|