@swarmclawai/swarmclaw 0.6.3 → 0.6.6
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 +5 -3
- package/package.json +5 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +41 -2
- package/src/app/api/chatrooms/[id]/route.ts +15 -1
- package/src/app/api/chatrooms/route.ts +15 -2
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/tasks/route.ts +24 -0
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/app/page.tsx +7 -3
- package/src/cli/index.js +15 -0
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +1 -0
- package/src/components/agents/agent-chat-list.tsx +1 -1
- package/src/components/agents/agent-sheet.tsx +112 -26
- package/src/components/auth/access-key-gate.tsx +22 -11
- package/src/components/chat/chat-area.tsx +2 -2
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +14 -3
- package/src/components/chat/message-list.tsx +5 -4
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/tool-call-bubble.test.ts +28 -0
- package/src/components/chat/tool-call-bubble.tsx +13 -1
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +7 -6
- package/src/components/chatrooms/chatroom-message.tsx +1 -1
- package/src/components/chatrooms/chatroom-sheet.tsx +1 -1
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/home/home-view.tsx +2 -1
- package/src/components/input/chat-input.tsx +5 -4
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +23 -9
- package/src/components/logs/log-list.tsx +7 -7
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/new-session-sheet.tsx +4 -3
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-sheet.tsx +21 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-media-query.ts +30 -4
- package/src/lib/api-client.ts +6 -18
- package/src/lib/fetch-timeout.ts +17 -0
- package/src/lib/notification-sounds.ts +4 -4
- package/src/lib/safe-storage.ts +42 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/chat-execution.ts +35 -3
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +64 -11
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +80 -2
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +8 -5
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +226 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +6 -2
- package/src/lib/server/session-tools/memory.ts +1 -1
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web-output.test.ts +29 -0
- package/src/lib/server/session-tools/web-output.ts +16 -0
- package/src/lib/server/session-tools/web.ts +7 -3
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +38 -0
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/view-routes.ts +6 -1
- package/src/stores/use-app-store.ts +17 -11
- package/src/types/index.ts +60 -1
package/README.md
CHANGED
|
@@ -118,20 +118,20 @@ Notes:
|
|
|
118
118
|
- **Orchestration** — Multi-agent workflows powered by LangGraph with automatic sub-agent routing, checkpointed execution, and rich delegation cards that link to sub-agent chat threads
|
|
119
119
|
- **Agentic Execution Policy** — Tool-first autonomous action loop with progress updates, evidence-driven answers, and better use of platform tools for long-lived work
|
|
120
120
|
- **Runtime Date/Time Grounding** — Session, orchestrator, chatroom, and connector prompts include authoritative current timestamp context to reduce stale-date behavior
|
|
121
|
-
- **Task Board** — Queue and track agent tasks with status, comments,
|
|
121
|
+
- **Task Board** — Queue and track agent tasks with status, comments, structured result artifacts (`outputFiles`, uploads), completion reports, and archiving. Strict capability policy pauses tasks for human approval before tool execution
|
|
122
122
|
- **Task Metrics API** — Built-in analytics endpoint for WIP, cycle times, throughput velocity, completion/failure by agent, and priority distribution
|
|
123
123
|
- **Background Daemon** — Auto-processes queued tasks and scheduled jobs with a 30s heartbeat plus recurring health monitoring
|
|
124
124
|
- **Scheduling** — Cron-based agent scheduling with human-friendly presets
|
|
125
125
|
- **Loop Runtime Controls** — Switch between bounded and ongoing loops with configurable step caps, runtime guards, heartbeat cadence, and timeout budgets
|
|
126
126
|
- **Session Run Queue** — Per-session queued runs with followup/steer/collect modes, collect coalescing for bursty inputs, and run-state APIs
|
|
127
127
|
- **Chat Iteration Workflow** — Edit-and-resend user turns, fork a new session from any message, bookmark key messages, use contextual follow-up suggestion chips, and auto-continue after tool access grants
|
|
128
|
-
- **Agent Chatrooms** — Multi-agent room conversations with `@mention` routing, chained agent replies, reactions,
|
|
128
|
+
- **Agent Chatrooms** — Multi-agent room conversations with `@mention` routing, chained agent replies, reactions, file/image-aware context, health-aware member filtering, and persistent context compaction for long-lived rooms
|
|
129
129
|
- **Live Chat Telemetry** — Thinking/tool/responding stream phases, live main-loop status badges, connector activity presence, tone indicator, and optional sound notifications
|
|
130
130
|
- **Global Search Palette** — `Cmd/Ctrl+K` search across agents, tasks, sessions, schedules, webhooks, and skills from anywhere in the app
|
|
131
131
|
- **Notification Center** — Real-time in-app notifications for task/schedule/daemon events with unread tracking, mark-all/clear-read controls, and optional action links
|
|
132
132
|
- **Preview-Rich Chat UI** — Side preview panel for tool outputs (image/browser/html/code), inline code/PDF previews for attachments, and image lightbox support
|
|
133
133
|
- **Voice Settings** — Per-instance ElevenLabs API key + voice ID for TTS replies, plus configurable speech recognition language for chat input
|
|
134
|
-
- **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling
|
|
134
|
+
- **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling, inbound voice-note transcription (ElevenLabs/OpenAI fallback), and WhatsApp-friendly plain-text formatting
|
|
135
135
|
- **Skills System** — Discover local skills, import skills from URL, and load OpenClaw `SKILL.md` files (frontmatter-compatible)
|
|
136
136
|
- **Execution Logging** — Structured audit trail for triggers, tool calls, file ops, commits, and errors in a dedicated `logs.db`
|
|
137
137
|
- **Context Management** — Auto-compaction of conversation history when approaching context limits, with manual `context_status` and `context_summarize` tools for agents
|
|
@@ -157,6 +157,7 @@ CREDENTIAL_SECRET=<auto-generated> # AES-256 encryption key for stored credentia
|
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
Data is stored in `data/swarmclaw.db` (SQLite with WAL mode), `data/memory.db` (agent memory with FTS5 + vector embeddings), `data/logs.db` (execution audit trail), and `data/langgraph-checkpoints.db` (orchestrator checkpoints). Back the `data/` directory up if you care about your sessions, agents, and credentials. Existing JSON file data is auto-migrated to SQLite on first run.
|
|
160
|
+
Agent wallet private keys are stored encrypted (AES-256 via `CREDENTIAL_SECRET`) in `data/swarmclaw.db` and are never returned by wallet API responses; keep `data/` out of version control.
|
|
160
161
|
|
|
161
162
|
The app listens on two ports: `PORT` (default 3456) for the HTTP/SSE API, and `PORT + 1` (default 3457) for WebSocket push notifications. The WS port can be customized with `--ws-port`.
|
|
162
163
|
|
|
@@ -288,6 +289,7 @@ Agents can use the following tools when enabled:
|
|
|
288
289
|
| HTTP Request | Make direct API calls with method, headers, body, redirect control, and timeout |
|
|
289
290
|
| Git | Run structured git subcommands (`status`, `diff`, `log`, `add`, `commit`, `push`, etc.) with repo safety checks |
|
|
290
291
|
| Memory | Store and retrieve long-term memories with FTS5 + vector search, file references, image attachments, and linked memory graph traversal |
|
|
292
|
+
| Wallet | Manage an agent-linked Solana wallet (`wallet_tool`) to check balance/address, send SOL (limits + approval), and review transaction history |
|
|
291
293
|
| Sandbox | Run JS/TS (Deno) or Python code in an isolated sandbox. Created files are returned as downloadable artifacts |
|
|
292
294
|
| MCP Servers | Connect to external Model Context Protocol servers. Tools from MCP servers are injected as first-class agent tools |
|
|
293
295
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"build:ci": "NEXT_DISABLE_ESLINT=1 next build",
|
|
46
46
|
"start": "next start",
|
|
47
47
|
"start:standalone": "node .next/standalone/server.js",
|
|
48
|
+
"benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
|
|
48
49
|
"lint": "eslint",
|
|
49
50
|
"lint:fix": "eslint --fix",
|
|
50
51
|
"lint:baseline": "node ./scripts/lint-baseline.mjs check",
|
|
@@ -63,8 +64,10 @@
|
|
|
63
64
|
"@multiavatar/multiavatar": "^1.0.7",
|
|
64
65
|
"@playwright/mcp": "^0.0.68",
|
|
65
66
|
"@slack/bolt": "^4.6.0",
|
|
67
|
+
"@solana/web3.js": "^1.98.4",
|
|
66
68
|
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
|
67
69
|
"better-sqlite3": "^12.6.2",
|
|
70
|
+
"bs58": "^5.0.0",
|
|
68
71
|
"cheerio": "^1.2.0",
|
|
69
72
|
"class-variance-authority": "^0.7.1",
|
|
70
73
|
"clsx": "^2.1.1",
|
|
@@ -90,6 +93,7 @@
|
|
|
90
93
|
"recharts": "^3.7.0",
|
|
91
94
|
"rehype-highlight": "^7.0.2",
|
|
92
95
|
"remark-gfm": "^4.0.1",
|
|
96
|
+
"remove-markdown": "^0.6.3",
|
|
93
97
|
"sonner": "^2.0.7",
|
|
94
98
|
"tailwind-merge": "^3.4.1",
|
|
95
99
|
"ws": "^8.19.0",
|
|
@@ -8,11 +8,14 @@ import { getProvider } from '@/lib/providers'
|
|
|
8
8
|
import {
|
|
9
9
|
resolveApiKey,
|
|
10
10
|
parseMentions,
|
|
11
|
+
compactChatroomMessages,
|
|
11
12
|
buildChatroomSystemPrompt,
|
|
12
13
|
buildSyntheticSession,
|
|
13
14
|
buildAgentSystemPromptForChatroom,
|
|
14
15
|
buildHistoryForAgent,
|
|
15
16
|
} from '@/lib/server/chatroom-helpers'
|
|
17
|
+
import { filterHealthyChatroomAgents } from '@/lib/server/chatroom-health'
|
|
18
|
+
import { markProviderFailure, markProviderSuccess } from '@/lib/server/provider-health'
|
|
16
19
|
import type { Chatroom, ChatroomMessage, Agent } from '@/types'
|
|
17
20
|
|
|
18
21
|
export const dynamic = 'force-dynamic'
|
|
@@ -49,6 +52,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
49
52
|
if (chatroom.autoAddress && mentions.length === 0) {
|
|
50
53
|
mentions = [...chatroom.agentIds]
|
|
51
54
|
}
|
|
55
|
+
const mentionHealth = filterHealthyChatroomAgents(mentions, agents)
|
|
56
|
+
mentions = mentionHealth.healthyAgentIds
|
|
52
57
|
const userMessage: ChatroomMessage = {
|
|
53
58
|
id: genId(),
|
|
54
59
|
senderId,
|
|
@@ -63,6 +68,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
63
68
|
...(replyToId ? { replyToId } : {}),
|
|
64
69
|
}
|
|
65
70
|
chatroom.messages.push(userMessage)
|
|
71
|
+
compactChatroomMessages(chatroom)
|
|
66
72
|
chatroom.updatedAt = Date.now()
|
|
67
73
|
chatrooms[id] = chatroom
|
|
68
74
|
saveChatrooms(chatrooms)
|
|
@@ -94,6 +100,22 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
const processAgents = async () => {
|
|
103
|
+
if (mentionHealth.skipped.length > 0) {
|
|
104
|
+
const detail = mentionHealth.skipped
|
|
105
|
+
.map((row) => `${agents[row.agentId]?.name || row.agentId}: ${row.reason}`)
|
|
106
|
+
.join(', ')
|
|
107
|
+
writeEvent({ t: 'err', text: `Skipped agents: ${detail}` })
|
|
108
|
+
}
|
|
109
|
+
if (mentions.length === 0) {
|
|
110
|
+
writeEvent({ t: 'err', text: 'No healthy agents available in this chatroom. Check provider credentials/endpoints and retry.' })
|
|
111
|
+
writeEvent({ t: 'done' })
|
|
112
|
+
if (!closed) {
|
|
113
|
+
try { controller.close() } catch { /* already closed */ }
|
|
114
|
+
closed = true
|
|
115
|
+
}
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
97
119
|
// Build agent queue: start with mentioned agents, then chain
|
|
98
120
|
const initialQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = mentions.map((aid) => ({ agentId: aid, depth: 0 }))
|
|
99
121
|
const processed = new Set<string>()
|
|
@@ -128,6 +150,11 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
128
150
|
try {
|
|
129
151
|
const freshChatrooms = loadChatrooms()
|
|
130
152
|
const freshChatroom = freshChatrooms[id] as Chatroom
|
|
153
|
+
if (compactChatroomMessages(freshChatroom)) {
|
|
154
|
+
freshChatrooms[id] = freshChatroom
|
|
155
|
+
saveChatrooms(freshChatrooms)
|
|
156
|
+
notify(`chatroom:${id}`)
|
|
157
|
+
}
|
|
131
158
|
|
|
132
159
|
const syntheticSession = buildSyntheticSession(agent, id)
|
|
133
160
|
const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
|
|
@@ -170,16 +197,25 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
170
197
|
history,
|
|
171
198
|
})
|
|
172
199
|
|
|
173
|
-
const responseText = result.fullText || fullText
|
|
200
|
+
const responseText = result.finalResponse || result.fullText || fullText
|
|
174
201
|
|
|
175
202
|
// Don't persist empty or error-only messages — they pollute chat history
|
|
176
203
|
if (!responseText.trim() && agentError) {
|
|
204
|
+
markProviderFailure(agent.provider, agentError)
|
|
177
205
|
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
178
206
|
return []
|
|
179
207
|
}
|
|
180
208
|
|
|
181
209
|
if (responseText.trim()) {
|
|
182
|
-
const
|
|
210
|
+
const parsedMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
|
|
211
|
+
const chainedHealth = filterHealthyChatroomAgents(parsedMentions, agents)
|
|
212
|
+
const newMentions = chainedHealth.healthyAgentIds
|
|
213
|
+
if (chainedHealth.skipped.length > 0) {
|
|
214
|
+
const detail = chainedHealth.skipped
|
|
215
|
+
.map((row) => `${agents[row.agentId]?.name || row.agentId}: ${row.reason}`)
|
|
216
|
+
.join(', ')
|
|
217
|
+
writeEvent({ t: 'err', text: `Mentioned agents skipped: ${detail}`, agentId: agent.id, agentName: agent.name })
|
|
218
|
+
}
|
|
183
219
|
const agentMessage: ChatroomMessage = {
|
|
184
220
|
id: genId(),
|
|
185
221
|
senderId: agent.id,
|
|
@@ -198,16 +234,19 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
198
234
|
saveChatrooms(latestChatrooms)
|
|
199
235
|
notify(`chatroom:${id}`)
|
|
200
236
|
|
|
237
|
+
markProviderSuccess(agent.provider)
|
|
201
238
|
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
202
239
|
|
|
203
240
|
// Return chained agent IDs — enriched context is built below when queuing
|
|
204
241
|
return newMentions.filter((mid) => !processed.has(mid) && freshChatroom.agentIds.includes(mid))
|
|
205
242
|
}
|
|
206
243
|
|
|
244
|
+
markProviderSuccess(agent.provider)
|
|
207
245
|
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
208
246
|
return []
|
|
209
247
|
} catch (err: unknown) {
|
|
210
248
|
const msg = err instanceof Error ? err.message : String(err)
|
|
249
|
+
markProviderFailure(agent.provider, msg)
|
|
211
250
|
writeEvent({ t: 'err', text: `Agent ${agent.name} error: ${msg}`, agentId: agent.id })
|
|
212
251
|
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
213
252
|
return []
|
|
@@ -21,16 +21,30 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
21
21
|
|
|
22
22
|
if (body.name !== undefined) chatroom.name = body.name
|
|
23
23
|
if (body.description !== undefined) chatroom.description = body.description
|
|
24
|
+
if (body.chatMode !== undefined) {
|
|
25
|
+
chatroom.chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
|
|
26
|
+
}
|
|
27
|
+
if (body.autoAddress !== undefined) {
|
|
28
|
+
chatroom.autoAddress = Boolean(body.autoAddress)
|
|
29
|
+
}
|
|
24
30
|
|
|
25
31
|
// Diff agentIds and inject join/leave system messages
|
|
26
32
|
if (Array.isArray(body.agentIds)) {
|
|
33
|
+
const agents = loadAgents()
|
|
34
|
+
const invalidAgentIds = (body.agentIds as string[]).filter((agentId) => !agents[agentId])
|
|
35
|
+
if (invalidAgentIds.length > 0) {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: `Unknown chatroom member(s): ${invalidAgentIds.join(', ')}` },
|
|
38
|
+
{ status: 400 },
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
const oldIds = new Set(chatroom.agentIds)
|
|
28
43
|
const newIds = new Set(body.agentIds as string[])
|
|
29
44
|
const added = (body.agentIds as string[]).filter((aid: string) => !oldIds.has(aid))
|
|
30
45
|
const removed = chatroom.agentIds.filter((aid: string) => !newIds.has(aid))
|
|
31
46
|
|
|
32
47
|
if (added.length > 0 || removed.length > 0) {
|
|
33
|
-
const agents = loadAgents()
|
|
34
48
|
if (!Array.isArray(chatroom.messages)) chatroom.messages = []
|
|
35
49
|
const now = Date.now()
|
|
36
50
|
let offset = 0
|
|
@@ -16,11 +16,22 @@ export async function POST(req: Request) {
|
|
|
16
16
|
const chatrooms = loadChatrooms()
|
|
17
17
|
const id = genId()
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const requestedAgentIds: string[] = Array.isArray(body.agentIds) ? body.agentIds : []
|
|
20
|
+
const knownAgents = loadAgents()
|
|
21
|
+
const invalidAgentIds = requestedAgentIds.filter((agentId) => !knownAgents[agentId])
|
|
22
|
+
if (invalidAgentIds.length > 0) {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ error: `Unknown chatroom member(s): ${invalidAgentIds.join(', ')}` },
|
|
25
|
+
{ status: 400 },
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
const agentIds: string[] = requestedAgentIds
|
|
29
|
+
const chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
|
|
30
|
+
const autoAddress = Boolean(body.autoAddress)
|
|
20
31
|
const now = Date.now()
|
|
21
32
|
|
|
22
33
|
// Generate join messages for initial agents
|
|
23
|
-
const agents = agentIds.length > 0 ?
|
|
34
|
+
const agents = agentIds.length > 0 ? knownAgents : {}
|
|
24
35
|
const joinMessages: ChatroomMessage[] = agentIds.map((agentId: string, i: number) => ({
|
|
25
36
|
id: genId(),
|
|
26
37
|
senderId: 'system',
|
|
@@ -38,6 +49,8 @@ export async function POST(req: Request) {
|
|
|
38
49
|
description: body.description || '',
|
|
39
50
|
agentIds,
|
|
40
51
|
messages: joinMessages,
|
|
52
|
+
chatMode,
|
|
53
|
+
autoAddress,
|
|
41
54
|
createdAt: now,
|
|
42
55
|
updatedAt: now,
|
|
43
56
|
}
|
|
@@ -53,7 +53,10 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
|
|
|
53
53
|
existingTask.title = `[Sched] ${schedule.name} (run #${schedule.runNumber})`
|
|
54
54
|
existingTask.result = null
|
|
55
55
|
existingTask.error = null
|
|
56
|
+
existingTask.outputFiles = []
|
|
57
|
+
existingTask.artifacts = []
|
|
56
58
|
existingTask.sessionId = null
|
|
59
|
+
existingTask.completionReportPath = null
|
|
57
60
|
existingTask.updatedAt = now
|
|
58
61
|
existingTask.queuedAt = null
|
|
59
62
|
existingTask.startedAt = null
|
|
@@ -84,6 +84,30 @@ export async function POST(req: Request) {
|
|
|
84
84
|
sessionId: typeof body.sessionId === 'string' ? body.sessionId : null,
|
|
85
85
|
result: typeof body.result === 'string' ? body.result : null,
|
|
86
86
|
error: typeof body.error === 'string' ? body.error : null,
|
|
87
|
+
outputFiles: Array.isArray(body.outputFiles)
|
|
88
|
+
? body.outputFiles.filter((entry: unknown) => typeof entry === 'string').slice(0, 24)
|
|
89
|
+
: [],
|
|
90
|
+
artifacts: Array.isArray(body.artifacts)
|
|
91
|
+
? body.artifacts
|
|
92
|
+
.filter((artifact: unknown) => artifact && typeof artifact === 'object')
|
|
93
|
+
.map((artifact: unknown) => {
|
|
94
|
+
const row = artifact as {
|
|
95
|
+
url?: unknown
|
|
96
|
+
type?: unknown
|
|
97
|
+
filename?: unknown
|
|
98
|
+
}
|
|
99
|
+
const normalizedType = String(row.type || '')
|
|
100
|
+
return {
|
|
101
|
+
url: String(row.url || ''),
|
|
102
|
+
type: ['image', 'video', 'pdf', 'file'].includes(normalizedType)
|
|
103
|
+
? (normalizedType as 'image' | 'video' | 'pdf' | 'file')
|
|
104
|
+
: 'file',
|
|
105
|
+
filename: String(row.filename || ''),
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
.filter((artifact: { url: string; filename: string }) => artifact.url && artifact.filename)
|
|
109
|
+
.slice(0, 24)
|
|
110
|
+
: [],
|
|
87
111
|
createdAt: now,
|
|
88
112
|
updatedAt: now,
|
|
89
113
|
queuedAt: null,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
|
|
3
|
+
import { sendSol } from '@/lib/server/solana'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import type { AgentWallet, WalletTransaction } from '@/types'
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
9
|
+
const { id } = await params
|
|
10
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
11
|
+
const wallet = wallets[id]
|
|
12
|
+
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
13
|
+
|
|
14
|
+
const body = await req.json()
|
|
15
|
+
const transactionId = typeof body.transactionId === 'string' ? body.transactionId.trim() : ''
|
|
16
|
+
const decision = body.decision as 'approve' | 'deny'
|
|
17
|
+
|
|
18
|
+
if (!transactionId) {
|
|
19
|
+
return NextResponse.json({ error: 'transactionId is required' }, { status: 400 })
|
|
20
|
+
}
|
|
21
|
+
if (decision !== 'approve' && decision !== 'deny') {
|
|
22
|
+
return NextResponse.json({ error: 'decision must be "approve" or "deny"' }, { status: 400 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
|
|
26
|
+
const tx = allTxs[transactionId]
|
|
27
|
+
if (!tx || tx.walletId !== id) {
|
|
28
|
+
return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })
|
|
29
|
+
}
|
|
30
|
+
if (tx.status !== 'pending_approval') {
|
|
31
|
+
return NextResponse.json({ error: `Transaction is already ${tx.status}` }, { status: 409 })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (decision === 'deny') {
|
|
35
|
+
tx.status = 'denied'
|
|
36
|
+
tx.approvedBy = 'user'
|
|
37
|
+
upsertWalletTransaction(transactionId, tx)
|
|
38
|
+
notify('wallets')
|
|
39
|
+
return NextResponse.json({ status: 'denied', transactionId })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Approve — sign and submit
|
|
43
|
+
try {
|
|
44
|
+
const { signature, fee } = await sendSol(wallet.encryptedPrivateKey, tx.toAddress, tx.amountLamports)
|
|
45
|
+
tx.status = 'confirmed'
|
|
46
|
+
tx.signature = signature
|
|
47
|
+
tx.feeLamports = fee
|
|
48
|
+
tx.approvedBy = 'user'
|
|
49
|
+
upsertWalletTransaction(transactionId, tx)
|
|
50
|
+
notify('wallets')
|
|
51
|
+
return NextResponse.json({ status: 'confirmed', transactionId, signature })
|
|
52
|
+
} catch (err: unknown) {
|
|
53
|
+
tx.status = 'failed'
|
|
54
|
+
upsertWalletTransaction(transactionId, tx)
|
|
55
|
+
notify('wallets')
|
|
56
|
+
return NextResponse.json({
|
|
57
|
+
error: err instanceof Error ? err.message : String(err),
|
|
58
|
+
transactionId,
|
|
59
|
+
status: 'failed',
|
|
60
|
+
}, { status: 500 })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadWallets, loadWalletBalanceHistory } from '@/lib/server/storage'
|
|
3
|
+
import type { AgentWallet, WalletBalanceSnapshot } from '@/types'
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
7
|
+
const { id } = await params
|
|
8
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
9
|
+
const wallet = wallets[id]
|
|
10
|
+
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
11
|
+
|
|
12
|
+
const allSnapshots = loadWalletBalanceHistory() as Record<string, WalletBalanceSnapshot>
|
|
13
|
+
const walletSnapshots = Object.values(allSnapshots)
|
|
14
|
+
.filter((s) => s.walletId === id)
|
|
15
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
16
|
+
|
|
17
|
+
return NextResponse.json(walletSnapshots)
|
|
18
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadWallets, upsertWallet, deleteWallet as deleteWalletFromStore, loadAgents, saveAgents } from '@/lib/server/storage'
|
|
3
|
+
import { getBalance, lamportsToSol } from '@/lib/server/solana'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import type { AgentWallet } from '@/types'
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
function stripPrivateKey(wallet: Record<string, unknown>): Record<string, unknown> {
|
|
9
|
+
return Object.fromEntries(Object.entries(wallet).filter(([k]) => k !== 'encryptedPrivateKey'))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
13
|
+
const { id } = await params
|
|
14
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
15
|
+
const wallet = wallets[id]
|
|
16
|
+
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
17
|
+
|
|
18
|
+
// Fetch live on-chain balance
|
|
19
|
+
let balanceLamports = 0
|
|
20
|
+
let balanceSol = 0
|
|
21
|
+
try {
|
|
22
|
+
balanceLamports = await getBalance(wallet.publicKey)
|
|
23
|
+
balanceSol = lamportsToSol(balanceLamports)
|
|
24
|
+
} catch {
|
|
25
|
+
// RPC failure — return 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return NextResponse.json({
|
|
29
|
+
...stripPrivateKey(wallet as unknown as Record<string, unknown>),
|
|
30
|
+
balanceLamports,
|
|
31
|
+
balanceSol,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
36
|
+
const { id } = await params
|
|
37
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
38
|
+
const wallet = wallets[id]
|
|
39
|
+
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
40
|
+
|
|
41
|
+
const body = await req.json()
|
|
42
|
+
|
|
43
|
+
// Reassign wallet to a different agent
|
|
44
|
+
if (typeof body.agentId === 'string' && body.agentId !== wallet.agentId) {
|
|
45
|
+
const agents = loadAgents()
|
|
46
|
+
const newAgent = agents[body.agentId]
|
|
47
|
+
if (!newAgent) return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
|
48
|
+
|
|
49
|
+
// Check new agent doesn't already have a wallet
|
|
50
|
+
const allWallets = loadWallets() as Record<string, AgentWallet>
|
|
51
|
+
const conflict = Object.values(allWallets).find((w) => w.agentId === body.agentId && w.id !== id)
|
|
52
|
+
if (conflict) return NextResponse.json({ error: 'Target agent already has a wallet' }, { status: 409 })
|
|
53
|
+
|
|
54
|
+
// Unlink old agent
|
|
55
|
+
const oldAgent = agents[wallet.agentId]
|
|
56
|
+
if (oldAgent) {
|
|
57
|
+
oldAgent.walletId = null
|
|
58
|
+
oldAgent.updatedAt = Date.now()
|
|
59
|
+
agents[wallet.agentId] = oldAgent
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Link new agent
|
|
63
|
+
newAgent.walletId = id
|
|
64
|
+
newAgent.updatedAt = Date.now()
|
|
65
|
+
agents[body.agentId] = newAgent
|
|
66
|
+
saveAgents(agents)
|
|
67
|
+
notify('agents')
|
|
68
|
+
|
|
69
|
+
wallet.agentId = body.agentId
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (body.label !== undefined) wallet.label = body.label
|
|
73
|
+
if (typeof body.spendingLimitLamports === 'number') wallet.spendingLimitLamports = body.spendingLimitLamports
|
|
74
|
+
if (typeof body.dailyLimitLamports === 'number') wallet.dailyLimitLamports = body.dailyLimitLamports
|
|
75
|
+
if (typeof body.requireApproval === 'boolean') wallet.requireApproval = body.requireApproval
|
|
76
|
+
wallet.updatedAt = Date.now()
|
|
77
|
+
|
|
78
|
+
upsertWallet(id, wallet)
|
|
79
|
+
notify('wallets')
|
|
80
|
+
|
|
81
|
+
return NextResponse.json(stripPrivateKey(wallet as unknown as Record<string, unknown>))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
85
|
+
const { id } = await params
|
|
86
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
87
|
+
const wallet = wallets[id]
|
|
88
|
+
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
89
|
+
|
|
90
|
+
// Check if balance > 0 and warn
|
|
91
|
+
let balanceLamports = 0
|
|
92
|
+
try {
|
|
93
|
+
balanceLamports = await getBalance(wallet.publicKey)
|
|
94
|
+
} catch { /* ignore */ }
|
|
95
|
+
|
|
96
|
+
if (balanceLamports > 0) {
|
|
97
|
+
// Still delete, but include warning
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Unlink from agent
|
|
101
|
+
const agents = loadAgents()
|
|
102
|
+
const agent = agents[wallet.agentId]
|
|
103
|
+
if (agent) {
|
|
104
|
+
agent.walletId = null
|
|
105
|
+
agent.updatedAt = Date.now()
|
|
106
|
+
agents[wallet.agentId] = agent
|
|
107
|
+
saveAgents(agents)
|
|
108
|
+
notify('agents')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
deleteWalletFromStore(id)
|
|
112
|
+
notify('wallets')
|
|
113
|
+
|
|
114
|
+
return NextResponse.json({
|
|
115
|
+
ok: true,
|
|
116
|
+
warning: balanceLamports > 0 ? `Wallet had ${lamportsToSol(balanceLamports)} SOL remaining` : undefined,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
|
|
4
|
+
import { sendSol, isValidSolanaAddress, lamportsToSol } from '@/lib/server/solana'
|
|
5
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
import type { AgentWallet, WalletTransaction } from '@/types'
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
const { id } = await params
|
|
11
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
12
|
+
const wallet = wallets[id]
|
|
13
|
+
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
14
|
+
|
|
15
|
+
const body = await req.json()
|
|
16
|
+
const toAddress = typeof body.toAddress === 'string' ? body.toAddress.trim() : ''
|
|
17
|
+
const amountLamports = typeof body.amountLamports === 'number' ? Math.floor(body.amountLamports) : 0
|
|
18
|
+
const memo = typeof body.memo === 'string' ? body.memo.slice(0, 500) : undefined
|
|
19
|
+
|
|
20
|
+
if (!toAddress || !isValidSolanaAddress(toAddress)) {
|
|
21
|
+
return NextResponse.json({ error: 'Invalid recipient address' }, { status: 400 })
|
|
22
|
+
}
|
|
23
|
+
if (amountLamports <= 0) {
|
|
24
|
+
return NextResponse.json({ error: 'Amount must be positive' }, { status: 400 })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Per-tx spending limit
|
|
28
|
+
const perTxLimit = wallet.spendingLimitLamports ?? 100_000_000
|
|
29
|
+
if (amountLamports > perTxLimit) {
|
|
30
|
+
return NextResponse.json({
|
|
31
|
+
error: `Amount ${lamportsToSol(amountLamports)} SOL exceeds per-transaction limit of ${lamportsToSol(perTxLimit)} SOL`,
|
|
32
|
+
}, { status: 403 })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 24h rolling daily limit
|
|
36
|
+
const dailyLimit = wallet.dailyLimitLamports ?? 1_000_000_000
|
|
37
|
+
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
|
|
38
|
+
const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
|
|
39
|
+
const recentSends = Object.values(allTxs).filter(
|
|
40
|
+
(tx) => tx.walletId === id && tx.type === 'send' && tx.status === 'confirmed' && tx.timestamp > oneDayAgo,
|
|
41
|
+
)
|
|
42
|
+
const dailySpent = recentSends.reduce((sum, tx) => sum + tx.amountLamports, 0)
|
|
43
|
+
if (dailySpent + amountLamports > dailyLimit) {
|
|
44
|
+
return NextResponse.json({
|
|
45
|
+
error: `Daily limit exceeded. Spent ${lamportsToSol(dailySpent)} SOL in last 24h, limit is ${lamportsToSol(dailyLimit)} SOL`,
|
|
46
|
+
}, { status: 403 })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const txId = genId(8)
|
|
50
|
+
const now = Date.now()
|
|
51
|
+
|
|
52
|
+
// If requireApproval, create pending tx and return it
|
|
53
|
+
if (wallet.requireApproval) {
|
|
54
|
+
const pendingTx: WalletTransaction = {
|
|
55
|
+
id: txId,
|
|
56
|
+
walletId: id,
|
|
57
|
+
agentId: wallet.agentId,
|
|
58
|
+
chain: wallet.chain,
|
|
59
|
+
type: 'send',
|
|
60
|
+
signature: '',
|
|
61
|
+
fromAddress: wallet.publicKey,
|
|
62
|
+
toAddress,
|
|
63
|
+
amountLamports,
|
|
64
|
+
status: 'pending_approval',
|
|
65
|
+
memo,
|
|
66
|
+
timestamp: now,
|
|
67
|
+
}
|
|
68
|
+
upsertWalletTransaction(txId, pendingTx)
|
|
69
|
+
notify('wallets')
|
|
70
|
+
return NextResponse.json({ status: 'pending_approval', transactionId: txId, message: 'Transaction requires user approval' })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Auto-approved — sign and submit
|
|
74
|
+
try {
|
|
75
|
+
const { signature, fee } = await sendSol(wallet.encryptedPrivateKey, toAddress, amountLamports)
|
|
76
|
+
const confirmedTx: WalletTransaction = {
|
|
77
|
+
id: txId,
|
|
78
|
+
walletId: id,
|
|
79
|
+
agentId: wallet.agentId,
|
|
80
|
+
chain: wallet.chain,
|
|
81
|
+
type: 'send',
|
|
82
|
+
signature,
|
|
83
|
+
fromAddress: wallet.publicKey,
|
|
84
|
+
toAddress,
|
|
85
|
+
amountLamports,
|
|
86
|
+
feeLamports: fee,
|
|
87
|
+
status: 'confirmed',
|
|
88
|
+
memo,
|
|
89
|
+
approvedBy: 'auto',
|
|
90
|
+
timestamp: now,
|
|
91
|
+
}
|
|
92
|
+
upsertWalletTransaction(txId, confirmedTx)
|
|
93
|
+
notify('wallets')
|
|
94
|
+
return NextResponse.json({ status: 'confirmed', transactionId: txId, signature })
|
|
95
|
+
} catch (err: unknown) {
|
|
96
|
+
const failedTx: WalletTransaction = {
|
|
97
|
+
id: txId,
|
|
98
|
+
walletId: id,
|
|
99
|
+
agentId: wallet.agentId,
|
|
100
|
+
chain: wallet.chain,
|
|
101
|
+
type: 'send',
|
|
102
|
+
signature: '',
|
|
103
|
+
fromAddress: wallet.publicKey,
|
|
104
|
+
toAddress,
|
|
105
|
+
amountLamports,
|
|
106
|
+
status: 'failed',
|
|
107
|
+
memo,
|
|
108
|
+
timestamp: now,
|
|
109
|
+
}
|
|
110
|
+
upsertWalletTransaction(txId, failedTx)
|
|
111
|
+
notify('wallets')
|
|
112
|
+
return NextResponse.json({
|
|
113
|
+
error: err instanceof Error ? err.message : String(err),
|
|
114
|
+
transactionId: txId,
|
|
115
|
+
status: 'failed',
|
|
116
|
+
}, { status: 500 })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadWallets, loadWalletTransactions } from '@/lib/server/storage'
|
|
3
|
+
import type { AgentWallet, WalletTransaction } from '@/types'
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
7
|
+
const { id } = await params
|
|
8
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
9
|
+
const wallet = wallets[id]
|
|
10
|
+
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
11
|
+
|
|
12
|
+
const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
|
|
13
|
+
const walletTxs = Object.values(allTxs)
|
|
14
|
+
.filter((tx) => tx.walletId === id)
|
|
15
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
16
|
+
|
|
17
|
+
return NextResponse.json(walletTxs)
|
|
18
|
+
}
|