Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/autocrat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"react-router-dom": "^7.11.0",
"viem": "2.43.3",
"wagmi": "^2.17.4",
"zod": "4.2.1"
"zod": "4.2.1",
"@noble/hashes": "^2.0.1"
},
"devDependencies": {
"@jejunetwork/tests": "workspace:*",
Expand Down
84 changes: 54 additions & 30 deletions apps/bazaar/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export interface TFMMActionResponse {
success: boolean
txHash?: string
poolAddress?: Address
message?: string
error?: string
}

// API Error Handling
Expand All @@ -148,29 +150,25 @@ async function handleResponse<T>(
response: Response,
schema?: z.ZodType<T>,
): Promise<T> {
const data: T = await response.json()

if (!response.ok) {
const errorBody = await response.json()
const isErrorObject =
typeof errorBody === 'object' &&
errorBody !== null &&
!Array.isArray(errorBody)
typeof data === 'object' && data !== null && !Array.isArray(data)
const errorData = data as Record<string, unknown>
const message =
(isErrorObject &&
'error' in errorBody &&
typeof errorBody.error === 'string'
? errorBody.error
(isErrorObject && 'error' in errorData && typeof errorData.error === 'string'
? errorData.error
: null) ||
(isErrorObject &&
'message' in errorBody &&
typeof errorBody.message === 'string'
? errorBody.message
'message' in errorData &&
typeof errorData.message === 'string'
? errorData.message
: null) ||
`Request failed: ${response.status}`
throw new ApiError(message, response.status)
throw new ApiError(String(message), response.status, errorData)
}

const data: T = await response.json()

if (schema) {
const result = schema.safeParse(data)
if (!result.success) {
Expand Down Expand Up @@ -216,37 +214,63 @@ export const api = {
return handleResponse(response)
},

async createPool(params: {
tokens: Address[]
initialWeights: number[]
strategy: string
}): Promise<TFMMActionResponse> {
async createPool(
params: {
tokens: Address[]
initialWeights: number[]
strategy: string
name?: string
symbol?: string
swapFeeBps?: number
},
walletAddress?: Address,
): Promise<TFMMActionResponse> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (walletAddress) {
headers['x-wallet-address'] = walletAddress
}
const response = await fetch(`${API_BASE}/api/tfmm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers,
body: JSON.stringify({ action: 'create_pool', params }),
})
return handleResponse(response)
const data = await response.json()
// Return the response object even if not ok (it has success: false)
return data as TFMMActionResponse
},

async updateStrategy(params: {
poolAddress: Address
newStrategy: string
}): Promise<TFMMActionResponse> {
async updateStrategy(
params: {
poolAddress: Address
newStrategy: string
},
walletAddress?: Address,
): Promise<TFMMActionResponse> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (walletAddress) {
headers['x-wallet-address'] = walletAddress
}
const response = await fetch(`${API_BASE}/api/tfmm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers,
body: JSON.stringify({ action: 'update_strategy', params }),
})
return handleResponse(response)
},

async triggerRebalance(params: {
poolAddress: Address
}): Promise<TFMMActionResponse> {
async triggerRebalance(
params: {
poolAddress: Address
},
walletAddress?: Address,
): Promise<TFMMActionResponse> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (walletAddress) {
headers['x-wallet-address'] = walletAddress
}
const response = await fetch(`${API_BASE}/api/tfmm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers,
body: JSON.stringify({ action: 'trigger_rebalance', params }),
})
return handleResponse(response)
Expand Down
158 changes: 139 additions & 19 deletions apps/bazaar/api/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,19 @@ export function createBazaarApp(env?: Partial<BazaarEnv>) {
const isDev = env?.NETWORK === 'localnet'

const app = new Elysia()
.onError(({ code, error, path }) => {
.onError(({ code, error, path, set }) => {
// Log all errors for debugging
const msg = error instanceof Error ? error.message : String(error)
console.error(`[Bazaar] Error on ${path}:`, code, msg)
if (error instanceof Error && error.stack) {
console.error('[Bazaar] Error stack:', error.stack)
}
// Return proper error response
set.status = code === 'VALIDATION' ? 400 : code === 'NOT_FOUND' ? 404 : 500
return {
error: code === 'VALIDATION' ? 'validation_error' : 'internal_error',
message: msg,
}
})
.use(
cors({
Expand Down Expand Up @@ -810,37 +819,74 @@ export function createBazaarApp(env?: Partial<BazaarEnv>) {
)
}

const validated = expectValid(
TFMMPostRequestSchema,
body,
'TFMM POST request',
)
// Validate request body
let validated
try {
validated = expectValid(
TFMMPostRequestSchema,
body,
'TFMM POST request',
)
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error)
console.error('[Bazaar] TFMM request validation failed:', errorMessage)
console.error('[Bazaar] Request body:', JSON.stringify(body, null, 2))
return new Response(
JSON.stringify({
error: 'invalid_request',
message: errorMessage,
}),
{ status: 400, headers: { 'Content-Type': 'application/json' } },
)
}

// All write operations currently fail as contracts not deployed
// This prevents abuse while providing clear feedback
// Handle write operations
try {
switch (validated.action) {
case 'create_pool': {
await createTFMMPool(validated.params)
break // Never reached - createTFMMPool always throws
const result = await createTFMMPool(validated.params)
return new Response(
JSON.stringify({
success: true,
poolAddress: result.poolAddress,
message: result.message,
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
)
}

case 'update_strategy': {
await updatePoolStrategy(validated.params)
break // Never reached - updatePoolStrategy always throws
const result = await updatePoolStrategy(validated.params)
return new Response(
JSON.stringify({
success: true,
txHash: result.txData,
message: result.message,
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
)
}

case 'trigger_rebalance': {
await triggerPoolRebalance(validated.params)
break // Never reached - triggerPoolRebalance always throws
const result = await triggerPoolRebalance(validated.params)
return new Response(
JSON.stringify({
success: true,
txHash: result.txData,
message: result.message,
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
)
}
}
} catch (error) {
// Handle the service unavailable error from TFMM functions
// Handle errors from TFMM functions
const errorMessage =
error instanceof Error ? error.message : 'Service unavailable'
return new Response(
JSON.stringify({
success: false,
error: 'service_unavailable',
message: errorMessage,
}),
Expand All @@ -849,10 +895,13 @@ export function createBazaarApp(env?: Partial<BazaarEnv>) {
}

// This should never be reached
return new Response(JSON.stringify({ error: 'Unknown action' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
return new Response(
JSON.stringify({ success: false, error: 'Unknown action' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
},
)
}),
)

Expand Down Expand Up @@ -1040,6 +1089,77 @@ export function createBazaarApp(env?: Partial<BazaarEnv>) {
}),
)

// Fallback: serve static files from local dist in development
// This handles cases where static files aren't in IPFS yet
if (isDev) {
app.get('/*', async ({ path, set }) => {
// Skip API routes - they're handled above
if (
path.startsWith('/api/') ||
path === '/health' ||
path.startsWith('/.well-known/')
) {
set.status = 404
return { error: 'NOT_FOUND' }
}

// Try to serve from local dist/static directory
const { join, dirname } = await import('node:path')
const { existsSync, readFileSync } = await import('node:fs')
const { fileURLToPath } = await import('node:url')

let staticDir: string | null = null

// Find dist/static directory
if (typeof import.meta !== 'undefined' && 'dir' in import.meta && import.meta.dir) {
const apiDir = import.meta.dir
const bazaarDir = dirname(apiDir)
staticDir = join(bazaarDir, 'dist', 'static')
} else {
try {
const currentFile = fileURLToPath(import.meta.url)
const apiDir = dirname(currentFile)
const bazaarDir = dirname(apiDir)
staticDir = join(bazaarDir, 'dist', 'static')
} catch {
// Fallback to workspace root
staticDir = join(process.cwd(), 'apps', 'bazaar', 'dist', 'static')
}
}

if (staticDir && existsSync(staticDir)) {
const filePath = path === '/' ? 'index.html' : path.replace(/^\//, '')
const fullPath = join(staticDir, filePath)

if (existsSync(fullPath)) {
const content = readFileSync(fullPath)
const ext = filePath.split('.').pop()?.toLowerCase() ?? ''
const contentType =
ext === 'js'
? 'application/javascript'
: ext === 'css'
? 'text/css'
: ext === 'html'
? 'text/html'
: ext === 'svg'
? 'image/svg+xml'
: 'application/octet-stream'

return new Response(content, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'no-cache',
'X-Bazaar-Source': 'local-filesystem',
},
})
}
}

set.status = 404
return { error: 'NOT_FOUND' }
})
}

return app
}

Expand Down
Loading