repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | // ═══════════════════════════════════════════════════════════════ |
| #2 | // API ROUTER - Route handling for all endpoints |
| #3 | // Includes: Agent, Wallet, Smart Wallet, and GOAT Tool routes |
| #4 | // ═══════════════════════════════════════════════════════════════ |
| #5 | |
| #6 | import type { |
| #7 | Agent, |
| #8 | Env, |
| #9 | } from './index'; |
| #10 | import type { AgentService } from './services/agent'; |
| #11 | import type { CrossmintService } from './services/crossmint'; |
| #12 | import type { |
| #13 | DeploymentRequest, |
| #14 | DeploymentService, |
| #15 | } from './services/deployment'; |
| #16 | import { |
| #17 | type AgentExecutionContext, |
| #18 | AgentRuntimeService, |
| #19 | type ChatMessage, |
| #20 | } from './services/runtime'; |
| #21 | |
| #22 | // ───────────────────────────────────────────────── |
| #23 | // HELPERS |
| #24 | // ───────────────────────────────────────────────── |
| #25 | |
| #26 | function corsHeaders(env: Env): HeadersInit { |
| #27 | return { |
| #28 | 'Access-Control-Allow-Origin': env.CORS_ORIGIN || '*', |
| #29 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', |
| #30 | 'Access-Control-Allow-Headers': 'Content-Type, X-Agent-API-Key, X-Agent-Session', |
| #31 | }; |
| #32 | } |
| #33 | |
| #34 | function jsonResponse(data: unknown, status = 200, env: Env): Response { |
| #35 | return new Response(JSON.stringify(data), { |
| #36 | status, |
| #37 | headers: { |
| #38 | 'Content-Type': 'application/json', |
| #39 | ...corsHeaders(env), |
| #40 | }, |
| #41 | }); |
| #42 | } |
| #43 | |
| #44 | function errorResponse(message: string, status = 500, env: Env): Response { |
| #45 | return jsonResponse({ success: false, error: message }, status, env); |
| #46 | } |
| #47 | |
| #48 | // ───────────────────────────────────────────────── |
| #49 | // ROUTER CLASS |
| #50 | // ───────────────────────────────────────────────── |
| #51 | |
| #52 | export class Router { |
| #53 | private runtimeService: AgentRuntimeService; |
| #54 | |
| #55 | constructor( |
| #56 | private env: Env, |
| #57 | private agentService: AgentService, |
| #58 | private crossmintService: CrossmintService, |
| #59 | private deploymentService: DeploymentService |
| #60 | ) { |
| #61 | this.runtimeService = new AgentRuntimeService(env, env.DB); |
| #62 | } |
| #63 | |
| #64 | // ═══════════════════════════════════════════════════ |
| #65 | // FACTORY ROUTES (v1 API) |
| #66 | // ═══════════════════════════════════════════════════ |
| #67 | |
| #68 | async handleFactoryRoutes(request: Request, path: string): Promise<Response> { |
| #69 | const method = request.method; |
| #70 | const route = path.replace('/api/v1/factory', '') || '/'; |
| #71 | |
| #72 | // Protected routes (require auth) |
| #73 | const agent = await this.authenticate(request); |
| #74 | if (!agent) { |
| #75 | return errorResponse('Authentication required', 401, this.env); |
| #76 | } |
| #77 | |
| #78 | // Rate limit check |
| #79 | const rateLimitResult = await this.agentService.checkRateLimit(agent.id); |
| #80 | if (!rateLimitResult.allowed) { |
| #81 | return jsonResponse({ |
| #82 | success: false, |
| #83 | error: 'Rate limit exceeded', |
| #84 | remaining: rateLimitResult.remaining, |
| #85 | }, 429, this.env); |
| #86 | } |
| #87 | |
| #88 | await this.agentService.recordRequest(agent.id); |
| #89 | |
| #90 | // Factory route matching |
| #91 | if (route === '/deploy' && method === 'POST') { |
| #92 | return this.deployAgent(request, agent); |
| #93 | } |
| #94 | if (route === '/deployments' && method === 'GET') { |
| #95 | return this.getDeployments(agent); |
| #96 | } |
| #97 | if (route.startsWith('/deployments/') && route.endsWith('/stop') && method === 'POST') { |
| #98 | const deploymentId = route.replace('/deployments/', '').replace('/stop', ''); |
| #99 | return this.stopDeployment(deploymentId, agent); |
| #100 | } |
| #101 | if (route.startsWith('/deployments/') && method === 'GET') { |
| #102 | const deploymentId = route.replace('/deployments/', ''); |
| #103 | return this.getDeployment(deploymentId, agent); |
| #104 | } |
| #105 | if (route === '/info' && method === 'GET') { |
| #106 | return this.getFactoryInfo(); |
| #107 | } |
| #108 | |
| #109 | return errorResponse('Not Found', 404, this.env); |
| #110 | } |
| #111 | |
| #112 | private getFactoryInfo(): Response { |
| #113 | return jsonResponse({ |
| #114 | success: true, |
| #115 | data: { |
| #116 | service: 'Agent Factory API', |
| #117 | version: '1.0.0', |
| #118 | endpoints: { |
| #119 | deploy: 'POST /api/v1/factory/deploy', |
| #120 | deployments: 'GET /api/v1/factory/deployments', |
| #121 | deployment: 'GET /api/v1/factory/deployments/:id', |
| #122 | stop: 'POST /api/v1/factory/deployments/:id/stop', |
| #123 | }, |
| #124 | requiredFields: { |
| #125 | deploy: ['walletAddress', 'walletSignerType'], |
| #126 | }, |
| #127 | }, |
| #128 | }, 200, this.env); |
| #129 | } |
| #130 | |
| #131 | // ═══════════════════════════════════════════════════ |
| #132 | // AGENT ROUTES |
| #133 | // ═══════════════════════════════════════════════════ |
| #134 | |
| #135 | async handleAgentRoutes(request: Request, path: string): Promise<Response> { |
| #136 | const method = request.method; |
| #137 | const route = path.replace('/api/agents', '') || '/'; |
| #138 | |
| #139 | // Public routes (no auth required) |
| #140 | if (route === '/register' && method === 'POST') { |
| #141 | return this.register(request); |
| #142 | } |
| #143 | if (route === '/login' && method === 'POST') { |
| #144 | return this.login(request); |
| #145 | } |
| #146 | if (route === '/info' && method === 'GET') { |
| #147 | return this.getInfo(); |
| #148 | } |
| #149 | |
| #150 | // Protected routes (require auth) |
| #151 | const agent = await this.authenticate(request); |
| #152 | if (!agent) { |
| #153 | return errorResponse('Authentication required', 401, this.env); |
| #154 | } |
| #155 | |
| #156 | // Rate limit check |
| #157 | const rateLimitResult = await this.agentService.checkRateLimit(agent.id); |
| #158 | if (!rateLimitResult.allowed) { |
| #159 | return jsonResponse({ |
| #160 | success: false, |
| #161 | error: 'Rate limit exceeded', |
| #162 | remaining: rateLimitResult.remaining, |
| #163 | }, 429, this.env); |
| #164 | } |
| #165 | |
| #166 | // Record this request |
| #167 | await this.agentService.recordRequest(agent.id); |
| #168 | |
| #169 | // Route matching |
| #170 | if (route === '/me' && method === 'GET') { |
| #171 | return this.getMe(agent); |
| #172 | } |
| #173 | if (route === '/logout' && method === 'POST') { |
| #174 | return this.logout(request); |
| #175 | } |
| #176 | if (route === '/wallet/create' && method === 'POST') { |
| #177 | return this.createWallet(request, agent); |
| #178 | } |
| #179 | if (route === '/wallet' && method === 'GET') { |
| #180 | return this.getWallet(agent); |
| #181 | } |
| #182 | if (route === '/wallet/fund' && method === 'POST') { |
| #183 | return this.fundWallet(request, agent); |
| #184 | } |
| #185 | if (route === '/wallet/transfer' && method === 'POST') { |
| #186 | return this.transfer(request, agent); |
| #187 | } |
| #188 | if (route === '/rate-limit' && method === 'GET') { |
| #189 | return this.getRateLimit(agent); |
| #190 | } |
| #191 | if (route === '/api-key/regenerate' && method === 'POST') { |
| #192 | return this.regenerateApiKey(agent); |
| #193 | } |
| #194 | |
| #195 | // Deployment routes |
| #196 | if (route === '/deploy' && method === 'POST') { |
| #197 | return this.deployAgent(request, agent); |
| #198 | } |
| #199 | if (route === '/deployments' && method === 'GET') { |
| #200 | return this.getDeployments(agent); |
| #201 | } |
| #202 | if (route.startsWith('/deployments/') && method === 'GET') { |
| #203 | const deploymentId = route.replace('/deployments/', ''); |
| #204 | return this.getDeployment(deploymentId, agent); |
| #205 | } |
| #206 | if (route.startsWith('/deployments/') && route.endsWith('/stop') && method === 'POST') { |
| #207 | const deploymentId = route.replace('/deployments/', '').replace('/stop', ''); |
| #208 | return this.stopDeployment(deploymentId, agent); |
| #209 | } |
| #210 | if (route === '/signature-approval' && method === 'POST') { |
| #211 | return this.submitSignatureApproval(request, agent); |
| #212 | } |
| #213 | if (route === '/transaction-approval' && method === 'POST') { |
| #214 | return this.submitTransactionApproval(request, agent); |
| #215 | } |
| #216 | |
| #217 | // Chat and execution routes |
| #218 | if (route === '/chat' && method === 'POST') { |
| #219 | return this.chat(request, agent); |
| #220 | } |
| #221 | if (route === '/models' && method === 'GET') { |
| #222 | return this.getModels(); |
| #223 | } |
| #224 | if (route.match(/^\/deployments\/[^/]+\/chat$/) && method === 'POST') { |
| #225 | const deploymentId = route.replace('/deployments/', '').replace('/chat', ''); |
| #226 | return this.chatWithDeployment(request, deploymentId, agent); |
| #227 | } |
| #228 | if (route.match(/^\/deployments\/[^/]+\/execute-tool$/) && method === 'POST') { |
| #229 | const deploymentId = route.replace('/deployments/', '').replace('/execute-tool', ''); |
| #230 | return this.executeTool(request, deploymentId, agent); |
| #231 | } |
| #232 | |
| #233 | return errorResponse('Not Found', 404, this.env); |
| #234 | } |
| #235 | |
| #236 | // ═══════════════════════════════════════════════════ |
| #237 | // WALLET ROUTES (Direct Crossmint access) |
| #238 | // ═══════════════════════════════════════════════════ |
| #239 | |
| #240 | async handleWalletRoutes(request: Request, path: string): Promise<Response> { |
| #241 | const method = request.method; |
| #242 | const route = path.replace('/api/wallets', '') || '/'; |
| #243 | |
| #244 | if (route === '/info' && method === 'GET') { |
| #245 | return this.getCrossmintInfo(); |
| #246 | } |
| #247 | |
| #248 | return errorResponse('Not Found', 404, this.env); |
| #249 | } |
| #250 | |
| #251 | // ═══════════════════════════════════════════════════ |
| #252 | // SMART WALLET ROUTES (Advanced Crossmint + GOAT) |
| #253 | // ═══════════════════════════════════════════════════ |
| #254 | |
| #255 | async handleSmartWalletRoutes(request: Request, path: string): Promise<Response> { |
| #256 | const method = request.method; |
| #257 | const route = path.replace('/api/smart-wallets', '') || '/'; |
| #258 | |
| #259 | // Public routes |
| #260 | if (route === '/info' && method === 'GET') { |
| #261 | return this.getSmartWalletInfo(); |
| #262 | } |
| #263 | if (route === '/tools' && method === 'GET') { |
| #264 | return this.getGoatTools(); |
| #265 | } |
| #266 | |
| #267 | // Protected routes (require auth) |
| #268 | const agent = await this.authenticate(request); |
| #269 | if (!agent) { |
| #270 | return errorResponse('Authentication required', 401, this.env); |
| #271 | } |
| #272 | |
| #273 | // Rate limit check |
| #274 | const rateLimitResult = await this.agentService.checkRateLimit(agent.id); |
| #275 | if (!rateLimitResult.allowed) { |
| #276 | return jsonResponse({ |
| #277 | success: false, |
| #278 | error: 'Rate limit exceeded', |
| #279 | remaining: rateLimitResult.remaining, |
| #280 | }, 429, this.env); |
| #281 | } |
| #282 | |
| #283 | await this.agentService.recordRequest(agent.id); |
| #284 | |
| #285 | // Smart wallet creation routes |
| #286 | if (route === '/create-smart' && method === 'POST') { |
| #287 | return this.createSmartWallet(request, agent); |
| #288 | } |
| #289 | if (route === '/create-mpc' && method === 'POST') { |
| #290 | return this.createMpcWallet(request, agent); |
| #291 | } |
| #292 | if (route === '/get-or-create' && method === 'POST') { |
| #293 | return this.getOrCreateSmartWallet(request, agent); |
| #294 | } |
| #295 | |
| #296 | // Wallet operations |
| #297 | if (route.match(/^\/[^/]+\/balances$/) && method === 'GET') { |
| #298 | const address = route.split('/')[1]; |
| #299 | return this.getSmartWalletBalances(address, agent); |
| #300 | } |
| #301 | if (route.match(/^\/[^/]+\/airdrop$/) && method === 'POST') { |
| #302 | const address = route.split('/')[1]; |
| #303 | return this.airdropToSmartWallet(request, address, agent); |
| #304 | } |
| #305 | if (route.match(/^\/[^/]+\/transfer$/) && method === 'POST') { |
| #306 | const address = route.split('/')[1]; |
| #307 | return this.smartWalletTransfer(request, address, agent); |
| #308 | } |
| #309 | |
| #310 | // Delegated signer routes |
| #311 | if (route.match(/^\/[^/]+\/signers$/) && method === 'POST') { |
| #312 | const address = route.split('/')[1]; |
| #313 | return this.registerDelegatedSigner(request, address, agent); |
| #314 | } |
| #315 | if (route.match(/^\/[^/]+\/signers\/[^/]+\/approve$/) && method === 'POST') { |
| #316 | const parts = route.split('/'); |
| #317 | const address = parts[1]; |
| #318 | const signerId = parts[3]; |
| #319 | return this.approveDelegatedSigner(request, address, signerId, agent); |
| #320 | } |
| #321 | |
| #322 | // GOAT tool execution |
| #323 | if (route === '/execute-tool' && method === 'POST') { |
| #324 | return this.executeGoatTool(request, agent); |
| #325 | } |
| #326 | if (route.match(/^\/[^/]+\/execute-tool$/) && method === 'POST') { |
| #327 | const address = route.split('/')[1]; |
| #328 | return this.executeGoatToolWithWallet(request, address, agent); |
| #329 | } |
| #330 | |
| #331 | return errorResponse('Not Found', 404, this.env); |
| #332 | } |
| #333 | |
| #334 | // ───────────────────────────────────────────────── |
| #335 | // SMART WALLET ENDPOINTS |
| #336 | // ───────────────────────────────────────────────── |
| #337 | |
| #338 | private getSmartWalletInfo(): Response { |
| #339 | const tools = this.crossmintService.getAvailableGoatTools(); |
| #340 | |
| #341 | return jsonResponse({ |
| #342 | success: true, |
| #343 | data: { |
| #344 | service: 'Smart Wallet API', |
| #345 | version: '1.0.0', |
| #346 | walletTypes: ['smart', 'mpc'], |
| #347 | features: [ |
| #348 | 'Solana Smart Wallets with admin signer', |
| #349 | 'MPC Wallets (custodial)', |
| #350 | 'Delegated signer support', |
| #351 | 'GOAT SDK tool integration', |
| #352 | 'Jupiter swap quotes', |
| #353 | 'CoinGecko price feeds', |
| #354 | 'Devnet airdrops', |
| #355 | ], |
| #356 | goatTools: tools.map(t => t.name), |
| #357 | endpoints: { |
| #358 | createSmart: 'POST /api/smart-wallets/create-smart', |
| #359 | createMpc: 'POST /api/smart-wallets/create-mpc', |
| #360 | getOrCreate: 'POST /api/smart-wallets/get-or-create', |
| #361 | balances: 'GET /api/smart-wallets/:address/balances', |
| #362 | airdrop: 'POST /api/smart-wallets/:address/airdrop', |
| #363 | transfer: 'POST /api/smart-wallets/:address/transfer', |
| #364 | registerSigner: 'POST /api/smart-wallets/:address/signers', |
| #365 | approveSigner: 'POST /api/smart-wallets/:address/signers/:signerId/approve', |
| #366 | executeTool: 'POST /api/smart-wallets/execute-tool', |
| #367 | executeToolWithWallet: 'POST /api/smart-wallets/:address/execute-tool', |
| #368 | }, |
| #369 | }, |
| #370 | }, 200, this.env); |
| #371 | } |
| #372 | |
| #373 | private getGoatTools(): Response { |
| #374 | const tools = this.crossmintService.getAvailableGoatTools(); |
| #375 | |
| #376 | return jsonResponse({ |
| #377 | success: true, |
| #378 | data: { tools }, |
| #379 | }, 200, this.env); |
| #380 | } |
| #381 | |
| #382 | private async createSmartWallet(request: Request, agent: Agent): Promise<Response> { |
| #383 | try { |
| #384 | if (!agent.permissions.canCreateWallet) { |
| #385 | return errorResponse('Agent does not have permission to create wallets', 403, this.env); |
| #386 | } |
| #387 | |
| #388 | const body = await request.json() as { |
| #389 | adminSignerAddress?: string; |
| #390 | linkedUser?: string; |
| #391 | chain?: 'solana' | 'solana-devnet'; |
| #392 | }; |
| #393 | |
| #394 | const wallet = await this.crossmintService.createSmartWallet({ |
| #395 | adminSignerAddress: body.adminSignerAddress, |
| #396 | linkedUser: body.linkedUser || agent.id, |
| #397 | chain: body.chain || agent.chain, |
| #398 | }); |
| #399 | |
| #400 | // Log activity |
| #401 | await this.agentService.logActivity(agent.id, 'smart_wallet_create', { |
| #402 | address: wallet.address, |
| #403 | type: 'smart', |
| #404 | chain: wallet.chain, |
| #405 | }, request.headers.get('CF-Connecting-IP')); |
| #406 | |
| #407 | return jsonResponse({ |
| #408 | success: true, |
| #409 | data: { wallet }, |
| #410 | }, 201, this.env); |
| #411 | } catch (error) { |
| #412 | console.error('Create smart wallet error:', error); |
| #413 | return errorResponse( |
| #414 | error instanceof Error ? error.message : 'Failed to create smart wallet', |
| #415 | 500, |
| #416 | this.env |
| #417 | ); |
| #418 | } |
| #419 | } |
| #420 | |
| #421 | private async createMpcWallet(request: Request, agent: Agent): Promise<Response> { |
| #422 | try { |
| #423 | if (!agent.permissions.canCreateWallet) { |
| #424 | return errorResponse('Agent does not have permission to create wallets', 403, this.env); |
| #425 | } |
| #426 | |
| #427 | const body = await request.json() as { |
| #428 | identifier?: string; |
| #429 | alias?: string; |
| #430 | chain?: 'solana' | 'solana-devnet'; |
| #431 | }; |
| #432 | |
| #433 | const wallet = await this.crossmintService.createMpcWallet({ |
| #434 | identifier: body.identifier || agent.id, |
| #435 | alias: body.alias, |
| #436 | chain: body.chain || agent.chain, |
| #437 | }); |
| #438 | |
| #439 | // Log activity |
| #440 | await this.agentService.logActivity(agent.id, 'mpc_wallet_create', { |
| #441 | address: wallet.address, |
| #442 | type: 'mpc', |
| #443 | chain: wallet.chain, |
| #444 | }, request.headers.get('CF-Connecting-IP')); |
| #445 | |
| #446 | return jsonResponse({ |
| #447 | success: true, |
| #448 | data: { wallet }, |
| #449 | }, 201, this.env); |
| #450 | } catch (error) { |
| #451 | console.error('Create MPC wallet error:', error); |
| #452 | return errorResponse( |
| #453 | error instanceof Error ? error.message : 'Failed to create MPC wallet', |
| #454 | 500, |
| #455 | this.env |
| #456 | ); |
| #457 | } |
| #458 | } |
| #459 | |
| #460 | private async getOrCreateSmartWallet(request: Request, agent: Agent): Promise<Response> { |
| #461 | try { |
| #462 | if (!agent.permissions.canCreateWallet) { |
| #463 | return errorResponse('Agent does not have permission to create wallets', 403, this.env); |
| #464 | } |
| #465 | |
| #466 | const body = await request.json() as { |
| #467 | adminSignerAddress?: string; |
| #468 | linkedUser?: string; |
| #469 | chain?: 'solana' | 'solana-devnet'; |
| #470 | }; |
| #471 | |
| #472 | const wallet = await this.crossmintService.getOrCreateSmartWallet({ |
| #473 | adminSignerAddress: body.adminSignerAddress, |
| #474 | linkedUser: body.linkedUser || agent.id, |
| #475 | chain: body.chain || agent.chain, |
| #476 | }); |
| #477 | |
| #478 | return jsonResponse({ |
| #479 | success: true, |
| #480 | data: { wallet }, |
| #481 | }, 200, this.env); |
| #482 | } catch (error) { |
| #483 | console.error('Get or create smart wallet error:', error); |
| #484 | return errorResponse( |
| #485 | error instanceof Error ? error.message : 'Failed to get or create wallet', |
| #486 | 500, |
| #487 | this.env |
| #488 | ); |
| #489 | } |
| #490 | } |
| #491 | |
| #492 | private async getSmartWalletBalances(address: string, agent: Agent): Promise<Response> { |
| #493 | try { |
| #494 | const balance = await this.crossmintService.goatGetBalance({ |
| #495 | address, |
| #496 | chain: agent.chain, |
| #497 | }); |
| #498 | |
| #499 | const usdcBalance = await this.crossmintService.goatGetBalance({ |
| #500 | address, |
| #501 | token: 'usdc', |
| #502 | chain: agent.chain, |
| #503 | }); |
| #504 | |
| #505 | return jsonResponse({ |
| #506 | success: true, |
| #507 | data: { |
| #508 | address, |
| #509 | chain: agent.chain, |
| #510 | balances: { |
| #511 | sol: balance, |
| #512 | usdc: usdcBalance, |
| #513 | }, |
| #514 | }, |
| #515 | }, 200, this.env); |
| #516 | } catch (error) { |
| #517 | console.error('Get smart wallet balances error:', error); |
| #518 | return errorResponse( |
| #519 | error instanceof Error ? error.message : 'Failed to get balances', |
| #520 | 500, |
| #521 | this.env |
| #522 | ); |
| #523 | } |
| #524 | } |
| #525 | |
| #526 | private async airdropToSmartWallet( |
| #527 | request: Request, |
| #528 | address: string, |
| #529 | agent: Agent |
| #530 | ): Promise<Response> { |
| #531 | try { |
| #532 | if (agent.chain !== 'solana-devnet') { |
| #533 | return errorResponse('Airdrop only available on devnet', 400, this.env); |
| #534 | } |
| #535 | |
| #536 | const body = await request.json() as { amount?: number }; |
| #537 | |
| #538 | const result = await this.crossmintService.goatAirdropDevnet({ |
| #539 | address, |
| #540 | amount: body.amount, |
| #541 | }); |
| #542 | |
| #543 | // Log activity |
| #544 | await this.agentService.logActivity(agent.id, 'airdrop', { |
| #545 | address, |
| #546 | amount: result.amount, |
| #547 | signature: result.signature, |
| #548 | }, request.headers.get('CF-Connecting-IP')); |
| #549 | |
| #550 | return jsonResponse({ |
| #551 | success: true, |
| #552 | data: result, |
| #553 | }, 200, this.env); |
| #554 | } catch (error) { |
| #555 | console.error('Airdrop error:', error); |
| #556 | return errorResponse( |
| #557 | error instanceof Error ? error.message : 'Failed to airdrop', |
| #558 | 500, |
| #559 | this.env |
| #560 | ); |
| #561 | } |
| #562 | } |
| #563 | |
| #564 | private async smartWalletTransfer( |
| #565 | request: Request, |
| #566 | address: string, |
| #567 | agent: Agent |
| #568 | ): Promise<Response> { |
| #569 | try { |
| #570 | if (!agent.permissions.canTransfer) { |
| #571 | return errorResponse('Agent does not have permission to transfer', 403, this.env); |
| #572 | } |
| #573 | |
| #574 | const body = await request.json() as { |
| #575 | toAddress: string; |
| #576 | token: string; |
| #577 | amount: string; |
| #578 | signerType?: 'api-key' | 'delegated' | 'admin'; |
| #579 | }; |
| #580 | |
| #581 | if (!body.toAddress || !body.token || !body.amount) { |
| #582 | return errorResponse('toAddress, token, and amount are required', 400, this.env); |
| #583 | } |
| #584 | |
| #585 | // Check transfer limit |
| #586 | const amountNum = parseFloat(body.amount); |
| #587 | if (amountNum > agent.permissions.maxTransferAmount) { |
| #588 | return errorResponse( |
| #589 | `Transfer amount exceeds limit of $${agent.permissions.maxTransferAmount}`, |
| #590 | 403, |
| #591 | this.env |
| #592 | ); |
| #593 | } |
| #594 | |
| #595 | const result = await this.crossmintService.goatTransfer({ |
| #596 | fromAddress: address, |
| #597 | toAddress: body.toAddress, |
| #598 | token: body.token, |
| #599 | amount: body.amount, |
| #600 | chain: agent.chain, |
| #601 | signerType: body.signerType, |
| #602 | }); |
| #603 | |
| #604 | // Log activity |
| #605 | await this.agentService.logActivity(agent.id, 'smart_wallet_transfer', { |
| #606 | from: address, |
| #607 | to: body.toAddress, |
| #608 | token: body.token, |
| #609 | amount: body.amount, |
| #610 | txId: result.id, |
| #611 | }, request.headers.get('CF-Connecting-IP')); |
| #612 | |
| #613 | return jsonResponse({ |
| #614 | success: true, |
| #615 | data: result, |
| #616 | }, 200, this.env); |
| #617 | } catch (error) { |
| #618 | console.error('Smart wallet transfer error:', error); |
| #619 | return errorResponse( |
| #620 | error instanceof Error ? error.message : 'Transfer failed', |
| #621 | 500, |
| #622 | this.env |
| #623 | ); |
| #624 | } |
| #625 | } |
| #626 | |
| #627 | private async registerDelegatedSigner( |
| #628 | request: Request, |
| #629 | walletAddress: string, |
| #630 | agent: Agent |
| #631 | ): Promise<Response> { |
| #632 | try { |
| #633 | const body = await request.json() as { |
| #634 | signerAddress: string; |
| #635 | expiresAt?: string; |
| #636 | }; |
| #637 | |
| #638 | if (!body.signerAddress) { |
| #639 | return errorResponse('signerAddress is required', 400, this.env); |
| #640 | } |
| #641 | |
| #642 | const result = await this.crossmintService.registerDelegatedSigner({ |
| #643 | walletAddress, |
| #644 | signerAddress: body.signerAddress, |
| #645 | expiresAt: body.expiresAt, |
| #646 | }); |
| #647 | |
| #648 | // Log activity |
| #649 | await this.agentService.logActivity(agent.id, 'register_delegated_signer', { |
| #650 | walletAddress, |
| #651 | signerAddress: body.signerAddress, |
| #652 | signerId: result.id, |
| #653 | }, request.headers.get('CF-Connecting-IP')); |
| #654 | |
| #655 | return jsonResponse({ |
| #656 | success: true, |
| #657 | data: result, |
| #658 | }, 201, this.env); |
| #659 | } catch (error) { |
| #660 | console.error('Register delegated signer error:', error); |
| #661 | return errorResponse( |
| #662 | error instanceof Error ? error.message : 'Failed to register signer', |
| #663 | 500, |
| #664 | this.env |
| #665 | ); |
| #666 | } |
| #667 | } |
| #668 | |
| #669 | private async approveDelegatedSigner( |
| #670 | request: Request, |
| #671 | walletAddress: string, |
| #672 | signerId: string, |
| #673 | agent: Agent |
| #674 | ): Promise<Response> { |
| #675 | try { |
| #676 | const body = await request.json() as { |
| #677 | signerLocator: string; |
| #678 | signature: unknown; |
| #679 | metadata?: Record<string, unknown>; |
| #680 | }; |
| #681 | |
| #682 | if (!body.signerLocator || !body.signature) { |
| #683 | return errorResponse('signerLocator and signature are required', 400, this.env); |
| #684 | } |
| #685 | |
| #686 | const result = await this.crossmintService.approveDelegatedSigner({ |
| #687 | walletAddress, |
| #688 | signerId, |
| #689 | signerLocator: body.signerLocator, |
| #690 | signature: body.signature, |
| #691 | metadata: body.metadata, |
| #692 | }); |
| #693 | |
| #694 | // Log activity |
| #695 | await this.agentService.logActivity(agent.id, 'approve_delegated_signer', { |
| #696 | walletAddress, |
| #697 | signerId, |
| #698 | }, request.headers.get('CF-Connecting-IP')); |
| #699 | |
| #700 | return jsonResponse({ |
| #701 | success: true, |
| #702 | data: result, |
| #703 | }, 200, this.env); |
| #704 | } catch (error) { |
| #705 | console.error('Approve delegated signer error:', error); |
| #706 | return errorResponse( |
| #707 | error instanceof Error ? error.message : 'Failed to approve signer', |
| #708 | 500, |
| #709 | this.env |
| #710 | ); |
| #711 | } |
| #712 | } |
| #713 | |
| #714 | private async executeGoatTool(request: Request, agent: Agent): Promise<Response> { |
| #715 | try { |
| #716 | const body = await request.json() as { |
| #717 | tool: string; |
| #718 | params: Record<string, unknown>; |
| #719 | }; |
| #720 | |
| #721 | if (!body.tool) { |
| #722 | return errorResponse('tool is required', 400, this.env); |
| #723 | } |
| #724 | |
| #725 | const result = await this.crossmintService.executeGoatTool( |
| #726 | body.tool, |
| #727 | body.params || {}, |
| #728 | agent.wallet_address || undefined |
| #729 | ); |
| #730 | |
| #731 | // Log activity |
| #732 | await this.agentService.logActivity(agent.id, 'goat_tool_execute', { |
| #733 | tool: body.tool, |
| #734 | params: body.params, |
| #735 | }, request.headers.get('CF-Connecting-IP')); |
| #736 | |
| #737 | return jsonResponse({ |
| #738 | success: true, |
| #739 | data: { tool: body.tool, result }, |
| #740 | }, 200, this.env); |
| #741 | } catch (error) { |
| #742 | console.error('Execute GOAT tool error:', error); |
| #743 | return errorResponse( |
| #744 | error instanceof Error ? error.message : 'Tool execution failed', |
| #745 | 500, |
| #746 | this.env |
| #747 | ); |
| #748 | } |
| #749 | } |
| #750 | |
| #751 | private async executeGoatToolWithWallet( |
| #752 | request: Request, |
| #753 | walletAddress: string, |
| #754 | agent: Agent |
| #755 | ): Promise<Response> { |
| #756 | try { |
| #757 | const body = await request.json() as { |
| #758 | tool: string; |
| #759 | params: Record<string, unknown>; |
| #760 | }; |
| #761 | |
| #762 | if (!body.tool) { |
| #763 | return errorResponse('tool is required', 400, this.env); |
| #764 | } |
| #765 | |
| #766 | const result = await this.crossmintService.executeGoatTool( |
| #767 | body.tool, |
| #768 | body.params || {}, |
| #769 | walletAddress |
| #770 | ); |
| #771 | |
| #772 | // Log activity |
| #773 | await this.agentService.logActivity(agent.id, 'goat_tool_execute', { |
| #774 | tool: body.tool, |
| #775 | walletAddress, |
| #776 | params: body.params, |
| #777 | }, request.headers.get('CF-Connecting-IP')); |
| #778 | |
| #779 | return jsonResponse({ |
| #780 | success: true, |
| #781 | data: { tool: body.tool, walletAddress, result }, |
| #782 | }, 200, this.env); |
| #783 | } catch (error) { |
| #784 | console.error('Execute GOAT tool with wallet error:', error); |
| #785 | return errorResponse( |
| #786 | error instanceof Error ? error.message : 'Tool execution failed', |
| #787 | 500, |
| #788 | this.env |
| #789 | ); |
| #790 | } |
| #791 | } |
| #792 | |
| #793 | // ───────────────────────────────────────────────── |
| #794 | // AUTHENTICATION |
| #795 | // ───────────────────────────────────────────────── |
| #796 | |
| #797 | private async authenticate(request: Request): Promise<Agent | null> { |
| #798 | // Try API key first |
| #799 | const apiKey = request.headers.get('X-Agent-API-Key'); |
| #800 | if (apiKey) { |
| #801 | return this.agentService.validateApiKey(apiKey); |
| #802 | } |
| #803 | |
| #804 | // Try session token |
| #805 | const sessionToken = request.headers.get('X-Agent-Session'); |
| #806 | if (sessionToken) { |
| #807 | return this.agentService.validateSession(sessionToken); |
| #808 | } |
| #809 | |
| #810 | return null; |
| #811 | } |
| #812 | |
| #813 | // ───────────────────────────────────────────────── |
| #814 | // PUBLIC ENDPOINTS |
| #815 | // ───────────────────────────────────────────────── |
| #816 | |
| #817 | private async register(request: Request): Promise<Response> { |
| #818 | try { |
| #819 | const body = await request.json() as { |
| #820 | name?: string; |
| #821 | description?: string; |
| #822 | chain?: 'solana' | 'solana-devnet'; |
| #823 | metadata?: Record<string, unknown>; |
| #824 | }; |
| #825 | |
| #826 | if (!body.name || body.name.length < 3 || body.name.length > 50) { |
| #827 | return errorResponse('Agent name must be 3-50 characters', 400, this.env); |
| #828 | } |
| #829 | |
| #830 | const result = await this.agentService.register({ |
| #831 | name: body.name, |
| #832 | description: body.description, |
| #833 | chain: body.chain || 'solana-devnet', |
| #834 | metadata: body.metadata, |
| #835 | }); |
| #836 | |
| #837 | // Log activity |
| #838 | await this.agentService.logActivity(result.agent.id, 'register', { |
| #839 | name: body.name, |
| #840 | chain: body.chain || 'solana-devnet', |
| #841 | }, request.headers.get('CF-Connecting-IP')); |
| #842 | |
| #843 | return jsonResponse({ |
| #844 | success: true, |
| #845 | data: { |
| #846 | agent: { |
| #847 | id: result.agent.id, |
| #848 | name: result.agent.name, |
| #849 | description: result.agent.description, |
| #850 | chain: result.agent.chain, |
| #851 | status: result.agent.status, |
| #852 | createdAt: result.agent.created_at, |
| #853 | permissions: result.agent.permissions, |
| #854 | rateLimit: { |
| #855 | requestsPerMinute: result.agent.requests_per_minute, |
| #856 | requestsPerDay: result.agent.requests_per_day, |
| #857 | }, |
| #858 | }, |
| #859 | apiKey: result.apiKey, |
| #860 | message: 'Save your API key securely - it will not be shown again!', |
| #861 | }, |
| #862 | }, 201, this.env); |
| #863 | } catch (error) { |
| #864 | console.error('Registration error:', error); |
| #865 | return errorResponse( |
| #866 | error instanceof Error ? error.message : 'Registration failed', |
| #867 | 500, |
| #868 | this.env |
| #869 | ); |
| #870 | } |
| #871 | } |
| #872 | |
| #873 | private async login(request: Request): Promise<Response> { |
| #874 | try { |
| #875 | const body = await request.json() as { apiKey?: string }; |
| #876 | |
| #877 | if (!body.apiKey) { |
| #878 | return errorResponse('API key is required', 400, this.env); |
| #879 | } |
| #880 | |
| #881 | const ipAddress = request.headers.get('CF-Connecting-IP'); |
| #882 | const userAgent = request.headers.get('User-Agent'); |
| #883 | |
| #884 | const result = await this.agentService.login(body.apiKey, ipAddress, userAgent); |
| #885 | |
| #886 | if (!result) { |
| #887 | return errorResponse('Invalid API key', 401, this.env); |
| #888 | } |
| #889 | |
| #890 | // Log activity |
| #891 | await this.agentService.logActivity(result.agent.id, 'login', { |
| #892 | ip: ipAddress, |
| #893 | }, ipAddress); |
| #894 | |
| #895 | return jsonResponse({ |
| #896 | success: true, |
| #897 | data: { |
| #898 | agent: { |
| #899 | id: result.agent.id, |
| #900 | name: result.agent.name, |
| #901 | description: result.agent.description, |
| #902 | walletAddress: result.agent.wallet_address, |
| #903 | chain: result.agent.chain, |
| #904 | status: result.agent.status, |
| #905 | createdAt: result.agent.created_at, |
| #906 | lastActiveAt: result.agent.last_active_at, |
| #907 | permissions: result.agent.permissions, |
| #908 | rateLimit: { |
| #909 | requestsPerMinute: result.agent.requests_per_minute, |
| #910 | requestsPerDay: result.agent.requests_per_day, |
| #911 | }, |
| #912 | }, |
| #913 | sessionToken: result.sessionToken, |
| #914 | expiresAt: result.expiresAt, |
| #915 | }, |
| #916 | }, 200, this.env); |
| #917 | } catch (error) { |
| #918 | console.error('Login error:', error); |
| #919 | return errorResponse( |
| #920 | error instanceof Error ? error.message : 'Login failed', |
| #921 | 401, |
| #922 | this.env |
| #923 | ); |
| #924 | } |
| #925 | } |
| #926 | |
| #927 | private getInfo(): Response { |
| #928 | return jsonResponse({ |
| #929 | success: true, |
| #930 | data: { |
| #931 | service: 'AI Agent API', |
| #932 | version: '1.0.0', |
| #933 | environment: this.env.ENVIRONMENT, |
| #934 | features: [ |
| #935 | 'Agent registration with API keys', |
| #936 | 'Session-based authentication', |
| #937 | 'Crossmint wallet integration', |
| #938 | 'Rate limiting', |
| #939 | 'Activity logging', |
| #940 | ], |
| #941 | endpoints: { |
| #942 | register: 'POST /api/agents/register', |
| #943 | login: 'POST /api/agents/login', |
| #944 | me: 'GET /api/agents/me', |
| #945 | wallet: 'GET /api/agents/wallet', |
| #946 | createWallet: 'POST /api/agents/wallet/create', |
| #947 | fundWallet: 'POST /api/agents/wallet/fund', |
| #948 | transfer: 'POST /api/agents/wallet/transfer', |
| #949 | rateLimit: 'GET /api/agents/rate-limit', |
| #950 | regenerateKey: 'POST /api/agents/api-key/regenerate', |
| #951 | }, |
| #952 | }, |
| #953 | }, 200, this.env); |
| #954 | } |
| #955 | |
| #956 | // ───────────────────────────────────────────────── |
| #957 | // PROTECTED ENDPOINTS |
| #958 | // ───────────────────────────────────────────────── |
| #959 | |
| #960 | private getMe(agent: Agent): Response { |
| #961 | return jsonResponse({ |
| #962 | success: true, |
| #963 | data: { |
| #964 | id: agent.id, |
| #965 | name: agent.name, |
| #966 | description: agent.description, |
| #967 | walletAddress: agent.wallet_address, |
| #968 | chain: agent.chain, |
| #969 | status: agent.status, |
| #970 | createdAt: agent.created_at, |
| #971 | lastActiveAt: agent.last_active_at, |
| #972 | permissions: agent.permissions, |
| #973 | rateLimit: { |
| #974 | requestsPerMinute: agent.requests_per_minute, |
| #975 | requestsPerDay: agent.requests_per_day, |
| #976 | }, |
| #977 | metadata: agent.metadata, |
| #978 | }, |
| #979 | }, 200, this.env); |
| #980 | } |
| #981 | |
| #982 | private async logout(request: Request): Promise<Response> { |
| #983 | const sessionToken = request.headers.get('X-Agent-Session'); |
| #984 | if (sessionToken) { |
| #985 | await this.agentService.invalidateSession(sessionToken); |
| #986 | } |
| #987 | |
| #988 | return jsonResponse({ |
| #989 | success: true, |
| #990 | message: 'Logged out successfully', |
| #991 | }, 200, this.env); |
| #992 | } |
| #993 | |
| #994 | private async createWallet(request: Request, agent: Agent): Promise<Response> { |
| #995 | try { |
| #996 | if (!agent.permissions.canCreateWallet) { |
| #997 | return errorResponse('Agent does not have permission to create wallets', 403, this.env); |
| #998 | } |
| #999 | |
| #1000 | if (agent.wallet_address) { |
| #1001 | return jsonResponse({ |
| #1002 | success: false, |
| #1003 | error: 'Agent already has a wallet', |
| #1004 | walletAddress: agent.wallet_address, |
| #1005 | }, 400, this.env); |
| #1006 | } |
| #1007 | |
| #1008 | const body = await request.json() as { alias?: string }; |
| #1009 | |
| #1010 | const wallet = await this.crossmintService.createWallet({ |
| #1011 | identifier: agent.id, |
| #1012 | chain: agent.chain, |
| #1013 | alias: body.alias || agent.name.toLowerCase().replace(/\s+/g, '-'), |
| #1014 | }); |
| #1015 | |
| #1016 | // Update agent with wallet address |
| #1017 | await this.agentService.updateWallet(agent.id, wallet.address); |
| #1018 | |
| #1019 | // Log activity |
| #1020 | await this.agentService.logActivity(agent.id, 'wallet_create', { |
| #1021 | address: wallet.address, |
| #1022 | chain: agent.chain, |
| #1023 | }, request.headers.get('CF-Connecting-IP')); |
| #1024 | |
| #1025 | return jsonResponse({ |
| #1026 | success: true, |
| #1027 | data: { |
| #1028 | wallet, |
| #1029 | message: 'Wallet created successfully', |
| #1030 | }, |
| #1031 | }, 201, this.env); |
| #1032 | } catch (error) { |
| #1033 | console.error('Create wallet error:', error); |
| #1034 | return errorResponse( |
| #1035 | error instanceof Error ? error.message : 'Failed to create wallet', |
| #1036 | 500, |
| #1037 | this.env |
| #1038 | ); |
| #1039 | } |
| #1040 | } |
| #1041 | |
| #1042 | private async getWallet(agent: Agent): Promise<Response> { |
| #1043 | try { |
| #1044 | if (!agent.wallet_address) { |
| #1045 | return errorResponse('Agent does not have a wallet', 404, this.env); |
| #1046 | } |
| #1047 | |
| #1048 | // Use wallet address for lookup (Crossmint doesn't support locator lookup for MPC wallets) |
| #1049 | const wallet = await this.crossmintService.getWalletByAddress(agent.wallet_address); |
| #1050 | const balances = await this.crossmintService.getBalancesByAddress(agent.wallet_address, agent.chain); |
| #1051 | |
| #1052 | return jsonResponse({ |
| #1053 | success: true, |
| #1054 | data: { wallet, balances }, |
| #1055 | }, 200, this.env); |
| #1056 | } catch (error) { |
| #1057 | console.error('Get wallet error:', error); |
| #1058 | return errorResponse( |
| #1059 | error instanceof Error ? error.message : 'Failed to get wallet', |
| #1060 | 500, |
| #1061 | this.env |
| #1062 | ); |
| #1063 | } |
| #1064 | } |
| #1065 | |
| #1066 | private async fundWallet(request: Request, agent: Agent): Promise<Response> { |
| #1067 | try { |
| #1068 | if (!agent.wallet_address) { |
| #1069 | return errorResponse('Agent does not have a wallet', 404, this.env); |
| #1070 | } |
| #1071 | |
| #1072 | if (agent.chain !== 'solana-devnet') { |
| #1073 | return errorResponse('Funding is only available on devnet', 400, this.env); |
| #1074 | } |
| #1075 | |
| #1076 | const body = await request.json() as { amount?: number }; |
| #1077 | const amount = body.amount || 10; |
| #1078 | |
| #1079 | const result = await this.crossmintService.fundWallet(agent.id, amount); |
| #1080 | |
| #1081 | // Log activity |
| #1082 | await this.agentService.logActivity(agent.id, 'fund', { |
| #1083 | amount, |
| #1084 | chain: agent.chain, |
| #1085 | }, request.headers.get('CF-Connecting-IP')); |
| #1086 | |
| #1087 | return jsonResponse({ |
| #1088 | success: true, |
| #1089 | data: result, |
| #1090 | }, 200, this.env); |
| #1091 | } catch (error) { |
| #1092 | console.error('Fund wallet error:', error); |
| #1093 | return errorResponse( |
| #1094 | error instanceof Error ? error.message : 'Failed to fund wallet', |
| #1095 | 500, |
| #1096 | this.env |
| #1097 | ); |
| #1098 | } |
| #1099 | } |
| #1100 | |
| #1101 | private async transfer(request: Request, agent: Agent): Promise<Response> { |
| #1102 | try { |
| #1103 | if (!agent.wallet_address) { |
| #1104 | return errorResponse('Agent does not have a wallet', 404, this.env); |
| #1105 | } |
| #1106 | |
| #1107 | if (!agent.permissions.canTransfer) { |
| #1108 | return errorResponse('Agent does not have permission to transfer', 403, this.env); |
| #1109 | } |
| #1110 | |
| #1111 | const body = await request.json() as { |
| #1112 | toAddress?: string; |
| #1113 | token?: string; |
| #1114 | amount?: string; |
| #1115 | }; |
| #1116 | |
| #1117 | if (!body.toAddress || !body.token || !body.amount) { |
| #1118 | return errorResponse('toAddress, token, and amount are required', 400, this.env); |
| #1119 | } |
| #1120 | |
| #1121 | // Check transfer limit |
| #1122 | const amountNum = parseFloat(body.amount); |
| #1123 | if (amountNum > agent.permissions.maxTransferAmount) { |
| #1124 | return errorResponse( |
| #1125 | `Transfer amount exceeds limit of $${agent.permissions.maxTransferAmount}`, |
| #1126 | 403, |
| #1127 | this.env |
| #1128 | ); |
| #1129 | } |
| #1130 | |
| #1131 | const result = await this.crossmintService.transfer({ |
| #1132 | fromIdentifier: agent.id, |
| #1133 | toAddress: body.toAddress, |
| #1134 | token: body.token, |
| #1135 | amount: body.amount, |
| #1136 | chain: agent.chain, |
| #1137 | }); |
| #1138 | |
| #1139 | // Log activity |
| #1140 | await this.agentService.logActivity(agent.id, 'transfer', { |
| #1141 | to: body.toAddress, |
| #1142 | token: body.token, |
| #1143 | amount: body.amount, |
| #1144 | txId: result.id, |
| #1145 | }, request.headers.get('CF-Connecting-IP')); |
| #1146 | |
| #1147 | return jsonResponse({ |
| #1148 | success: true, |
| #1149 | data: result, |
| #1150 | }, 200, this.env); |
| #1151 | } catch (error) { |
| #1152 | console.error('Transfer error:', error); |
| #1153 | return errorResponse( |
| #1154 | error instanceof Error ? error.message : 'Transfer failed', |
| #1155 | 500, |
| #1156 | this.env |
| #1157 | ); |
| #1158 | } |
| #1159 | } |
| #1160 | |
| #1161 | private async getRateLimit(agent: Agent): Promise<Response> { |
| #1162 | const rateLimitResult = await this.agentService.checkRateLimit(agent.id); |
| #1163 | |
| #1164 | return jsonResponse({ |
| #1165 | success: true, |
| #1166 | data: { |
| #1167 | limits: { |
| #1168 | requestsPerMinute: agent.requests_per_minute, |
| #1169 | requestsPerDay: agent.requests_per_day, |
| #1170 | }, |
| #1171 | remaining: rateLimitResult.remaining, |
| #1172 | }, |
| #1173 | }, 200, this.env); |
| #1174 | } |
| #1175 | |
| #1176 | private async regenerateApiKey(agent: Agent): Promise<Response> { |
| #1177 | try { |
| #1178 | const newApiKey = await this.agentService.regenerateApiKey(agent.id); |
| #1179 | |
| #1180 | // Log activity |
| #1181 | await this.agentService.logActivity(agent.id, 'api_key_regenerate', {}, null); |
| #1182 | |
| #1183 | return jsonResponse({ |
| #1184 | success: true, |
| #1185 | data: { |
| #1186 | apiKey: newApiKey, |
| #1187 | message: 'API key regenerated. Save it securely!', |
| #1188 | }, |
| #1189 | }, 200, this.env); |
| #1190 | } catch (error) { |
| #1191 | console.error('Regenerate API key error:', error); |
| #1192 | return errorResponse( |
| #1193 | error instanceof Error ? error.message : 'Failed to regenerate API key', |
| #1194 | 500, |
| #1195 | this.env |
| #1196 | ); |
| #1197 | } |
| #1198 | } |
| #1199 | |
| #1200 | // ───────────────────────────────────────────────── |
| #1201 | // CROSSMINT INFO |
| #1202 | // ───────────────────────────────────────────────── |
| #1203 | |
| #1204 | private getCrossmintInfo(): Response { |
| #1205 | const isConfigured = !!this.env.CROSSMINT_SERVERSIDE_API_KEY; |
| #1206 | const environment = this.env.CROSSMINT_SERVERSIDE_API_KEY?.startsWith('sk_staging_') |
| #1207 | ? 'staging' |
| #1208 | : 'production'; |
| #1209 | |
| #1210 | return jsonResponse({ |
| #1211 | success: true, |
| #1212 | data: { |
| #1213 | configured: isConfigured, |
| #1214 | environment, |
| #1215 | hasClientKey: !!this.env.CROSSMINT_CLIENTSIDE_API_KEY, |
| #1216 | }, |
| #1217 | }, 200, this.env); |
| #1218 | } |
| #1219 | |
| #1220 | // ═══════════════════════════════════════════════════ |
| #1221 | // DEPLOYMENT ENDPOINTS |
| #1222 | // ═══════════════════════════════════════════════════ |
| #1223 | |
| #1224 | private async deployAgent(request: Request, agent: Agent): Promise<Response> { |
| #1225 | try { |
| #1226 | const body = await request.json() as DeploymentRequest; |
| #1227 | |
| #1228 | if (!body.walletAddress || !body.walletSignerType) { |
| #1229 | return errorResponse('walletAddress and walletSignerType are required', 400, this.env); |
| #1230 | } |
| #1231 | |
| #1232 | const result = await this.deploymentService.deployAgent({ |
| #1233 | ...body, |
| #1234 | agentId: agent.id, |
| #1235 | }, agent); |
| #1236 | |
| #1237 | // Log activity |
| #1238 | await this.agentService.logActivity(agent.id, 'deploy', { |
| #1239 | deploymentId: result.deployment.id, |
| #1240 | walletAddress: body.walletAddress, |
| #1241 | }, request.headers.get('CF-Connecting-IP')); |
| #1242 | |
| #1243 | return jsonResponse({ |
| #1244 | success: true, |
| #1245 | data: { |
| #1246 | deployment: result.deployment, |
| #1247 | delegatedSignerMessage: result.delegatedSigner?.message, |
| #1248 | delegatedSignerId: result.delegatedSigner?.id, |
| #1249 | targetSignerLocator: result.delegatedSigner?.targetSignerLocator, |
| #1250 | delegatedSignerAlreadyActive: result.delegatedSigner?.alreadyActive || false, |
| #1251 | }, |
| #1252 | }, 201, this.env); |
| #1253 | } catch (error) { |
| #1254 | console.error('Deploy agent error:', error); |
| #1255 | return errorResponse( |
| #1256 | error instanceof Error ? error.message : 'Deployment failed', |
| #1257 | 500, |
| #1258 | this.env |
| #1259 | ); |
| #1260 | } |
| #1261 | } |
| #1262 | |
| #1263 | private async getDeployments(agent: Agent): Promise<Response> { |
| #1264 | try { |
| #1265 | const deployments = await this.deploymentService.getDeployments(agent.id); |
| #1266 | |
| #1267 | return jsonResponse({ |
| #1268 | success: true, |
| #1269 | data: { deployments }, |
| #1270 | }, 200, this.env); |
| #1271 | } catch (error) { |
| #1272 | console.error('Get deployments error:', error); |
| #1273 | return errorResponse( |
| #1274 | error instanceof Error ? error.message : 'Failed to get deployments', |
| #1275 | 500, |
| #1276 | this.env |
| #1277 | ); |
| #1278 | } |
| #1279 | } |
| #1280 | |
| #1281 | private async getDeployment(deploymentId: string, agent: Agent): Promise<Response> { |
| #1282 | try { |
| #1283 | const deployment = await this.deploymentService.getDeployment(deploymentId); |
| #1284 | |
| #1285 | if (!deployment) { |
| #1286 | return errorResponse('Deployment not found', 404, this.env); |
| #1287 | } |
| #1288 | |
| #1289 | if (deployment.agentId !== agent.id) { |
| #1290 | return errorResponse('Unauthorized', 403, this.env); |
| #1291 | } |
| #1292 | |
| #1293 | return jsonResponse({ |
| #1294 | success: true, |
| #1295 | data: { deployment }, |
| #1296 | }, 200, this.env); |
| #1297 | } catch (error) { |
| #1298 | console.error('Get deployment error:', error); |
| #1299 | return errorResponse( |
| #1300 | error instanceof Error ? error.message : 'Failed to get deployment', |
| #1301 | 500, |
| #1302 | this.env |
| #1303 | ); |
| #1304 | } |
| #1305 | } |
| #1306 | |
| #1307 | private async stopDeployment(deploymentId: string, agent: Agent): Promise<Response> { |
| #1308 | try { |
| #1309 | const deployment = await this.deploymentService.getDeployment(deploymentId); |
| #1310 | |
| #1311 | if (!deployment) { |
| #1312 | return errorResponse('Deployment not found', 404, this.env); |
| #1313 | } |
| #1314 | |
| #1315 | if (deployment.agentId !== agent.id) { |
| #1316 | return errorResponse('Unauthorized', 403, this.env); |
| #1317 | } |
| #1318 | |
| #1319 | await this.deploymentService.stopDeployment(deploymentId); |
| #1320 | |
| #1321 | // Log activity |
| #1322 | await this.agentService.logActivity(agent.id, 'stop_deployment', { |
| #1323 | deploymentId, |
| #1324 | }, null); |
| #1325 | |
| #1326 | return jsonResponse({ |
| #1327 | success: true, |
| #1328 | message: 'Deployment stopped', |
| #1329 | }, 200, this.env); |
| #1330 | } catch (error) { |
| #1331 | console.error('Stop deployment error:', error); |
| #1332 | return errorResponse( |
| #1333 | error instanceof Error ? error.message : 'Failed to stop deployment', |
| #1334 | 500, |
| #1335 | this.env |
| #1336 | ); |
| #1337 | } |
| #1338 | } |
| #1339 | |
| #1340 | private async submitSignatureApproval(request: Request, agent: Agent): Promise<Response> { |
| #1341 | try { |
| #1342 | const body = await request.json() as { |
| #1343 | walletAddress: string; |
| #1344 | signatureId: string; |
| #1345 | signerLocator: string; |
| #1346 | signature: unknown; |
| #1347 | metadata?: unknown; |
| #1348 | }; |
| #1349 | |
| #1350 | if (!body.walletAddress || !body.signatureId || !body.signerLocator || !body.signature) { |
| #1351 | return errorResponse('Missing required fields', 400, this.env); |
| #1352 | } |
| #1353 | |
| #1354 | await this.deploymentService.submitSignatureApproval( |
| #1355 | body.walletAddress, |
| #1356 | body.signatureId, |
| #1357 | body.signerLocator, |
| #1358 | body.signature, |
| #1359 | body.metadata |
| #1360 | ); |
| #1361 | |
| #1362 | // Update deployment status if exists |
| #1363 | const deployments = await this.deploymentService.getDeployments(agent.id); |
| #1364 | for (const dep of deployments) { |
| #1365 | if (dep.delegatedSignerId === body.signatureId && dep.status === 'pending') { |
| #1366 | await this.deploymentService.updateDeploymentStatus(dep.id, 'running'); |
| #1367 | } |
| #1368 | } |
| #1369 | |
| #1370 | return jsonResponse({ |
| #1371 | success: true, |
| #1372 | message: 'Signature approval submitted', |
| #1373 | }, 200, this.env); |
| #1374 | } catch (error) { |
| #1375 | console.error('Signature approval error:', error); |
| #1376 | return errorResponse( |
| #1377 | error instanceof Error ? error.message : 'Failed to submit signature approval', |
| #1378 | 500, |
| #1379 | this.env |
| #1380 | ); |
| #1381 | } |
| #1382 | } |
| #1383 | |
| #1384 | private async submitTransactionApproval(request: Request, agent: Agent): Promise<Response> { |
| #1385 | try { |
| #1386 | const body = await request.json() as { |
| #1387 | walletAddress: string; |
| #1388 | transactionId: string; |
| #1389 | signerLocator: string; |
| #1390 | signature: string; |
| #1391 | }; |
| #1392 | |
| #1393 | if (!body.walletAddress || !body.transactionId || !body.signerLocator || !body.signature) { |
| #1394 | return errorResponse('Missing required fields', 400, this.env); |
| #1395 | } |
| #1396 | |
| #1397 | await this.deploymentService.submitTransactionApproval( |
| #1398 | body.walletAddress, |
| #1399 | body.transactionId, |
| #1400 | body.signerLocator, |
| #1401 | body.signature |
| #1402 | ); |
| #1403 | |
| #1404 | // Update deployment status if exists |
| #1405 | const deployments = await this.deploymentService.getDeployments(agent.id); |
| #1406 | for (const dep of deployments) { |
| #1407 | if (dep.delegatedSignerId === body.transactionId && dep.status === 'pending') { |
| #1408 | await this.deploymentService.updateDeploymentStatus(dep.id, 'running'); |
| #1409 | } |
| #1410 | } |
| #1411 | |
| #1412 | return jsonResponse({ |
| #1413 | success: true, |
| #1414 | message: 'Transaction approval submitted', |
| #1415 | }, 200, this.env); |
| #1416 | } catch (error) { |
| #1417 | console.error('Transaction approval error:', error); |
| #1418 | return errorResponse( |
| #1419 | error instanceof Error ? error.message : 'Failed to submit transaction approval', |
| #1420 | 500, |
| #1421 | this.env |
| #1422 | ); |
| #1423 | } |
| #1424 | } |
| #1425 | |
| #1426 | // ═══════════════════════════════════════════════════ |
| #1427 | // CHAT & EXECUTION ENDPOINTS |
| #1428 | // ═══════════════════════════════════════════════════ |
| #1429 | |
| #1430 | private getModels(): Response { |
| #1431 | const models = this.runtimeService.getAvailableModels(); |
| #1432 | return jsonResponse({ |
| #1433 | success: true, |
| #1434 | data: { models }, |
| #1435 | }, 200, this.env); |
| #1436 | } |
| #1437 | |
| #1438 | private async chat(request: Request, agent: Agent): Promise<Response> { |
| #1439 | try { |
| #1440 | const body = await request.json() as { |
| #1441 | messages: ChatMessage[]; |
| #1442 | model?: string; |
| #1443 | temperature?: number; |
| #1444 | maxTokens?: number; |
| #1445 | tools?: boolean; |
| #1446 | stream?: boolean; |
| #1447 | }; |
| #1448 | |
| #1449 | if (!body.messages || !Array.isArray(body.messages)) { |
| #1450 | return errorResponse('messages array is required', 400, this.env); |
| #1451 | } |
| #1452 | |
| #1453 | // Use agent's default configuration |
| #1454 | const context: AgentExecutionContext = { |
| #1455 | agentId: agent.id, |
| #1456 | deploymentId: 'direct-chat', |
| #1457 | walletAddress: agent.wallet_address || undefined, |
| #1458 | chain: agent.chain, |
| #1459 | configuration: { |
| #1460 | model: body.model || 'gpt-4', |
| #1461 | maxTokens: body.maxTokens || 4096, |
| #1462 | temperature: body.temperature || 0.7, |
| #1463 | }, |
| #1464 | capabilities: ['chat', 'analyze'], |
| #1465 | }; |
| #1466 | |
| #1467 | const result = await this.runtimeService.chat(context, body.messages, { |
| #1468 | tools: body.tools, |
| #1469 | stream: body.stream, |
| #1470 | }); |
| #1471 | |
| #1472 | if (!result.success) { |
| #1473 | return errorResponse(result.error || 'Chat failed', 500, this.env); |
| #1474 | } |
| #1475 | |
| #1476 | return jsonResponse({ |
| #1477 | success: true, |
| #1478 | data: { |
| #1479 | response: result.response, |
| #1480 | toolCalls: result.toolCalls, |
| #1481 | tokensUsed: result.tokensUsed, |
| #1482 | executionTimeMs: result.executionTimeMs, |
| #1483 | }, |
| #1484 | }, 200, this.env); |
| #1485 | } catch (error) { |
| #1486 | console.error('Chat error:', error); |
| #1487 | return errorResponse( |
| #1488 | error instanceof Error ? error.message : 'Chat failed', |
| #1489 | 500, |
| #1490 | this.env |
| #1491 | ); |
| #1492 | } |
| #1493 | } |
| #1494 | |
| #1495 | private async chatWithDeployment( |
| #1496 | request: Request, |
| #1497 | deploymentId: string, |
| #1498 | agent: Agent |
| #1499 | ): Promise<Response> { |
| #1500 | try { |
| #1501 | const deployment = await this.deploymentService.getDeployment(deploymentId); |
| #1502 | |
| #1503 | if (!deployment) { |
| #1504 | return errorResponse('Deployment not found', 404, this.env); |
| #1505 | } |
| #1506 | |
| #1507 | if (deployment.agentId !== agent.id) { |
| #1508 | return errorResponse('Unauthorized', 403, this.env); |
| #1509 | } |
| #1510 | |
| #1511 | if (deployment.status !== 'running') { |
| #1512 | return errorResponse('Deployment is not running', 400, this.env); |
| #1513 | } |
| #1514 | |
| #1515 | const body = await request.json() as { |
| #1516 | messages: ChatMessage[]; |
| #1517 | tools?: boolean; |
| #1518 | stream?: boolean; |
| #1519 | responseFormat?: { type: 'json_object' | 'text' }; |
| #1520 | }; |
| #1521 | |
| #1522 | if (!body.messages || !Array.isArray(body.messages)) { |
| #1523 | return errorResponse('messages array is required', 400, this.env); |
| #1524 | } |
| #1525 | |
| #1526 | const context: AgentExecutionContext = { |
| #1527 | agentId: agent.id, |
| #1528 | deploymentId: deployment.id, |
| #1529 | walletAddress: deployment.walletAddress, |
| #1530 | chain: deployment.chain, |
| #1531 | configuration: deployment.configuration, |
| #1532 | capabilities: deployment.capabilities, |
| #1533 | }; |
| #1534 | |
| #1535 | const result = await this.runtimeService.chat(context, body.messages, { |
| #1536 | tools: body.tools, |
| #1537 | stream: body.stream, |
| #1538 | responseFormat: body.responseFormat, |
| #1539 | }); |
| #1540 | |
| #1541 | if (!result.success) { |
| #1542 | return errorResponse(result.error || 'Chat failed', 500, this.env); |
| #1543 | } |
| #1544 | |
| #1545 | // Log activity |
| #1546 | await this.agentService.logActivity(agent.id, 'chat', { |
| #1547 | deploymentId, |
| #1548 | tokensUsed: result.tokensUsed, |
| #1549 | }, null); |
| #1550 | |
| #1551 | return jsonResponse({ |
| #1552 | success: true, |
| #1553 | data: { |
| #1554 | response: result.response, |
| #1555 | toolCalls: result.toolCalls, |
| #1556 | tokensUsed: result.tokensUsed, |
| #1557 | executionTimeMs: result.executionTimeMs, |
| #1558 | }, |
| #1559 | }, 200, this.env); |
| #1560 | } catch (error) { |
| #1561 | console.error('Chat with deployment error:', error); |
| #1562 | return errorResponse( |
| #1563 | error instanceof Error ? error.message : 'Chat failed', |
| #1564 | 500, |
| #1565 | this.env |
| #1566 | ); |
| #1567 | } |
| #1568 | } |
| #1569 | |
| #1570 | private async executeTool( |
| #1571 | request: Request, |
| #1572 | deploymentId: string, |
| #1573 | agent: Agent |
| #1574 | ): Promise<Response> { |
| #1575 | try { |
| #1576 | const deployment = await this.deploymentService.getDeployment(deploymentId); |
| #1577 | |
| #1578 | if (!deployment) { |
| #1579 | return errorResponse('Deployment not found', 404, this.env); |
| #1580 | } |
| #1581 | |
| #1582 | if (deployment.agentId !== agent.id) { |
| #1583 | return errorResponse('Unauthorized', 403, this.env); |
| #1584 | } |
| #1585 | |
| #1586 | if (deployment.status !== 'running') { |
| #1587 | return errorResponse('Deployment is not running', 400, this.env); |
| #1588 | } |
| #1589 | |
| #1590 | const body = await request.json() as { |
| #1591 | toolCall: { |
| #1592 | id: string; |
| #1593 | type: 'function'; |
| #1594 | function: { |
| #1595 | name: string; |
| #1596 | arguments: string; |
| #1597 | }; |
| #1598 | }; |
| #1599 | }; |
| #1600 | |
| #1601 | if (!body.toolCall) { |
| #1602 | return errorResponse('toolCall is required', 400, this.env); |
| #1603 | } |
| #1604 | |
| #1605 | const context: AgentExecutionContext = { |
| #1606 | agentId: agent.id, |
| #1607 | deploymentId: deployment.id, |
| #1608 | walletAddress: deployment.walletAddress, |
| #1609 | chain: deployment.chain, |
| #1610 | configuration: deployment.configuration, |
| #1611 | capabilities: deployment.capabilities, |
| #1612 | }; |
| #1613 | |
| #1614 | const result = await this.runtimeService.executeTool(context, body.toolCall); |
| #1615 | |
| #1616 | // Log activity |
| #1617 | await this.agentService.logActivity(agent.id, 'execute_tool', { |
| #1618 | deploymentId, |
| #1619 | tool: body.toolCall.function.name, |
| #1620 | }, null); |
| #1621 | |
| #1622 | return jsonResponse({ |
| #1623 | success: true, |
| #1624 | data: { |
| #1625 | result: result.result, |
| #1626 | error: result.error, |
| #1627 | }, |
| #1628 | }, 200, this.env); |
| #1629 | } catch (error) { |
| #1630 | console.error('Execute tool error:', error); |
| #1631 | return errorResponse( |
| #1632 | error instanceof Error ? error.message : 'Tool execution failed', |
| #1633 | 500, |
| #1634 | this.env |
| #1635 | ); |
| #1636 | } |
| #1637 | } |
| #1638 | } |
| #1639 |