@swarmclawai/swarmclaw 0.7.7 → 0.8.0

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.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -0,0 +1,337 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { genId } from '@/lib/id'
4
+ import { computeTaskFingerprint } from '@/lib/task-dedupe'
5
+ import { formatZodError } from '@/lib/validation/schemas'
6
+ import { loadSettings, loadTasks, logActivity, upsertStoredItems } from '@/lib/server/storage'
7
+ import { notify } from '@/lib/server/ws-hub'
8
+ import type { BoardTask } from '@/types'
9
+
10
+ const MAX_IMPORT_LIMIT = 200
11
+ const BODY_CHAR_LIMIT = 12_000
12
+
13
+ const GitHubIssueImportSchema = z.object({
14
+ repo: z.string().trim().min(1, 'Repository is required'),
15
+ token: z.string().trim().optional().default(''),
16
+ state: z.enum(['open', 'closed', 'all']).optional().default('open'),
17
+ limit: z.coerce.number().int().min(1).max(MAX_IMPORT_LIMIT).optional().default(25),
18
+ labels: z.array(z.string()).optional().default([]),
19
+ projectId: z.string().trim().nullable().optional().default(null),
20
+ agentId: z.string().trim().nullable().optional().default(null),
21
+ })
22
+
23
+ type GitHubIssueLabel = string | { name?: string | null }
24
+
25
+ interface GitHubIssueRecord {
26
+ id: number | string
27
+ number: number
28
+ title: string
29
+ body?: string | null
30
+ state?: string | null
31
+ html_url?: string | null
32
+ labels?: GitHubIssueLabel[]
33
+ assignee?: { login?: string | null } | null
34
+ user?: { login?: string | null } | null
35
+ pull_request?: unknown
36
+ }
37
+
38
+ interface ParsedRepo {
39
+ owner: string
40
+ repo: string
41
+ fullName: string
42
+ }
43
+
44
+ function getGitHubToken(explicitToken: string): string {
45
+ return explicitToken.trim()
46
+ || process.env.GITHUB_TOKEN
47
+ || process.env.GH_TOKEN
48
+ || process.env.GITHUB_PERSONAL_ACCESS_TOKEN
49
+ || ''
50
+ }
51
+
52
+ function normalizeLabelName(label: GitHubIssueLabel): string {
53
+ if (typeof label === 'string') return label.trim()
54
+ return String(label?.name || '').trim()
55
+ }
56
+
57
+ function normalizeTag(value: string): string {
58
+ return value.trim().replace(/\s+/g, ' ').slice(0, 60)
59
+ }
60
+
61
+ function toIssueSummary(issue: GitHubIssueRecord, taskId?: string) {
62
+ return {
63
+ taskId,
64
+ number: issue.number,
65
+ title: issue.title || `Issue ${issue.number}`,
66
+ url: issue.html_url || null,
67
+ }
68
+ }
69
+
70
+ export function parseGitHubRepoInput(input: string): ParsedRepo | null {
71
+ const trimmed = input.trim().replace(/\.git$/i, '')
72
+ if (!trimmed) return null
73
+
74
+ if (/^https?:\/\//i.test(trimmed)) {
75
+ try {
76
+ const url = new URL(trimmed)
77
+ if (!/github\.com$/i.test(url.hostname)) return null
78
+ const parts = url.pathname.split('/').filter(Boolean)
79
+ if (parts.length < 2) return null
80
+ const owner = parts[0]
81
+ const repo = parts[1].replace(/\.git$/i, '')
82
+ if (!owner || !repo) return null
83
+ return { owner, repo, fullName: `${owner}/${repo}` }
84
+ } catch {
85
+ return null
86
+ }
87
+ }
88
+
89
+ const compact = trimmed.replace(/^github\.com\//i, '')
90
+ const parts = compact.split('/').filter(Boolean)
91
+ if (parts.length < 2) return null
92
+ const owner = parts[0]
93
+ const repo = parts[1].replace(/\.git$/i, '')
94
+ if (!owner || !repo) return null
95
+ return { owner, repo, fullName: `${owner}/${repo}` }
96
+ }
97
+
98
+ export function buildGitHubIssueTaskTitle(issue: GitHubIssueRecord, repoFullName: string): string {
99
+ const title = issue.title?.trim() || `Issue ${issue.number}`
100
+ return `[${repoFullName}#${issue.number}] ${title}`
101
+ }
102
+
103
+ export function buildGitHubIssueTaskDescription(issue: GitHubIssueRecord, repoFullName: string): string {
104
+ const labels = (issue.labels || [])
105
+ .map(normalizeLabelName)
106
+ .filter(Boolean)
107
+ const header = [
108
+ `Imported from GitHub issue ${repoFullName}#${issue.number}`,
109
+ issue.html_url ? `URL: ${issue.html_url}` : '',
110
+ issue.state ? `State: ${issue.state}` : '',
111
+ labels.length > 0 ? `Labels: ${labels.join(', ')}` : '',
112
+ issue.assignee?.login ? `Assignee: ${issue.assignee.login}` : '',
113
+ issue.user?.login ? `Opened by: ${issue.user.login}` : '',
114
+ ]
115
+ .filter(Boolean)
116
+ .join('\n')
117
+
118
+ const rawBody = String(issue.body || '').trim()
119
+ if (!rawBody) return header
120
+
121
+ const body = rawBody.length > BODY_CHAR_LIMIT
122
+ ? `${rawBody.slice(0, BODY_CHAR_LIMIT).trimEnd()}\n\n[Truncated during import]`
123
+ : rawBody
124
+
125
+ return `${header}\n\n${body}`
126
+ }
127
+
128
+ export function buildGitHubIssueTaskTags(issue: GitHubIssueRecord, repoFullName: string): string[] {
129
+ const raw = [
130
+ 'github',
131
+ repoFullName,
132
+ ...(issue.labels || []).map(normalizeLabelName),
133
+ ]
134
+ return Array.from(new Set(raw.map(normalizeTag).filter(Boolean))).slice(0, 8)
135
+ }
136
+
137
+ function findExistingImportedTask(
138
+ tasks: Record<string, BoardTask>,
139
+ repoFullName: string,
140
+ issueNumber: number,
141
+ ): BoardTask | null {
142
+ for (const task of Object.values(tasks)) {
143
+ if (task.sourceType !== 'import') continue
144
+ if (task.externalSource?.source !== 'github') continue
145
+ if (task.externalSource?.repo !== repoFullName) continue
146
+ if (task.externalSource?.number !== issueNumber) continue
147
+ return task
148
+ }
149
+ return null
150
+ }
151
+
152
+ async function fetchGitHubIssues(args: {
153
+ owner: string
154
+ repo: string
155
+ state: 'open' | 'closed' | 'all'
156
+ limit: number
157
+ labels: string[]
158
+ token: string
159
+ }): Promise<GitHubIssueRecord[]> {
160
+ const headers: Record<string, string> = {
161
+ Accept: 'application/vnd.github+json',
162
+ 'User-Agent': 'SwarmClaw',
163
+ 'X-GitHub-Api-Version': '2022-11-28',
164
+ }
165
+ if (args.token) headers.Authorization = `Bearer ${args.token}`
166
+
167
+ const results: GitHubIssueRecord[] = []
168
+ const perPage = Math.min(100, Math.max(30, args.limit))
169
+ const maxPages = Math.max(1, Math.ceil(args.limit / 100) + 2)
170
+
171
+ for (let page = 1; page <= maxPages && results.length < args.limit; page++) {
172
+ const url = new URL(`https://api.github.com/repos/${args.owner}/${args.repo}/issues`)
173
+ url.searchParams.set('state', args.state)
174
+ url.searchParams.set('per_page', String(perPage))
175
+ url.searchParams.set('page', String(page))
176
+ if (args.labels.length > 0) url.searchParams.set('labels', args.labels.join(','))
177
+
178
+ const response = await fetch(url, {
179
+ headers,
180
+ cache: 'no-store',
181
+ })
182
+
183
+ if (!response.ok) {
184
+ const payload = await response.json().catch(() => null) as { message?: unknown } | null
185
+ const message = typeof payload?.message === 'string'
186
+ ? payload.message
187
+ : `GitHub request failed (${response.status})`
188
+ const err = new Error(message) as Error & { status?: number }
189
+ err.status = response.status
190
+ throw err
191
+ }
192
+
193
+ const payload = await response.json().catch(() => null) as unknown
194
+ if (!Array.isArray(payload)) {
195
+ throw new Error('GitHub returned an unexpected response.')
196
+ }
197
+
198
+ const pageIssues = payload
199
+ .filter((entry): entry is GitHubIssueRecord => !!entry && typeof entry === 'object')
200
+ .filter((entry) => !entry.pull_request)
201
+
202
+ results.push(...pageIssues)
203
+ if (payload.length < perPage) break
204
+ }
205
+
206
+ return results.slice(0, args.limit)
207
+ }
208
+
209
+ export async function POST(req: Request) {
210
+ const raw = await req.json().catch(() => null)
211
+ const parsed = GitHubIssueImportSchema.safeParse(raw)
212
+ if (!parsed.success) {
213
+ return NextResponse.json(formatZodError(parsed.error), { status: 400 })
214
+ }
215
+
216
+ const repo = parseGitHubRepoInput(parsed.data.repo)
217
+ if (!repo) {
218
+ return NextResponse.json({ error: 'Use a GitHub repo like owner/repo or a github.com URL.' }, { status: 400 })
219
+ }
220
+
221
+ const labels = parsed.data.labels
222
+ .map((value) => String(value || '').trim())
223
+ .filter(Boolean)
224
+
225
+ let issues: GitHubIssueRecord[]
226
+ try {
227
+ issues = await fetchGitHubIssues({
228
+ owner: repo.owner,
229
+ repo: repo.repo,
230
+ state: parsed.data.state,
231
+ limit: parsed.data.limit,
232
+ labels,
233
+ token: getGitHubToken(parsed.data.token),
234
+ })
235
+ } catch (err) {
236
+ const message = err instanceof Error ? err.message : 'GitHub import failed.'
237
+ const status = typeof (err as { status?: unknown })?.status === 'number'
238
+ ? Number((err as { status?: number }).status)
239
+ : 500
240
+ const responseStatus = [400, 401, 403, 404, 429].includes(status) ? status : 502
241
+ return NextResponse.json({ error: message }, { status: responseStatus })
242
+ }
243
+
244
+ const tasks = loadTasks() as Record<string, BoardTask>
245
+ const settings = loadSettings()
246
+ const now = Date.now()
247
+ const maxAttempts = Math.max(1, Math.min(20, Math.trunc(Number(settings.defaultTaskMaxAttempts ?? 3))))
248
+ const retryBackoffSec = Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
249
+ const projectId = parsed.data.projectId || undefined
250
+ const agentId = parsed.data.agentId || ''
251
+
252
+ const created: Array<ReturnType<typeof toIssueSummary>> = []
253
+ const skipped: Array<ReturnType<typeof toIssueSummary>> = []
254
+ const taskEntries: Array<[string, BoardTask]> = []
255
+
256
+ for (const issue of issues) {
257
+ const existing = findExistingImportedTask(tasks, repo.fullName, issue.number)
258
+ if (existing) {
259
+ skipped.push(toIssueSummary(issue, existing.id))
260
+ continue
261
+ }
262
+
263
+ const id = genId()
264
+ const title = buildGitHubIssueTaskTitle(issue, repo.fullName)
265
+ const task: BoardTask = {
266
+ id,
267
+ title,
268
+ description: buildGitHubIssueTaskDescription(issue, repo.fullName),
269
+ status: 'backlog',
270
+ agentId,
271
+ projectId,
272
+ result: null,
273
+ error: null,
274
+ outputFiles: [],
275
+ artifacts: [],
276
+ createdAt: now,
277
+ updatedAt: now,
278
+ queuedAt: null,
279
+ startedAt: null,
280
+ completedAt: null,
281
+ archivedAt: null,
282
+ attempts: 0,
283
+ maxAttempts,
284
+ retryBackoffSec,
285
+ retryScheduledAt: null,
286
+ deadLetteredAt: null,
287
+ checkpoint: null,
288
+ blockedBy: [],
289
+ blocks: [],
290
+ tags: buildGitHubIssueTaskTags(issue, repo.fullName),
291
+ sourceType: 'import',
292
+ externalSource: {
293
+ source: 'github',
294
+ id: String(issue.id),
295
+ repo: repo.fullName,
296
+ number: issue.number,
297
+ state: issue.state || null,
298
+ labels: (issue.labels || []).map(normalizeLabelName).filter(Boolean),
299
+ assignee: issue.assignee?.login || null,
300
+ url: issue.html_url || null,
301
+ },
302
+ fingerprint: computeTaskFingerprint(title, agentId),
303
+ }
304
+
305
+ tasks[id] = task
306
+ taskEntries.push([id, task])
307
+ created.push(toIssueSummary(issue, id))
308
+ }
309
+
310
+ if (taskEntries.length > 0) {
311
+ upsertStoredItems('tasks', taskEntries)
312
+ notify('tasks')
313
+ }
314
+
315
+ logActivity({
316
+ entityType: 'task',
317
+ entityId: created[0]?.taskId || `github:${repo.fullName}`,
318
+ action: 'imported',
319
+ actor: 'user',
320
+ summary: `GitHub import from ${repo.fullName}: ${created.length} created, ${skipped.length} skipped`,
321
+ detail: {
322
+ repo: repo.fullName,
323
+ state: parsed.data.state,
324
+ labels,
325
+ created: created.length,
326
+ skipped: skipped.length,
327
+ },
328
+ })
329
+
330
+ return NextResponse.json({
331
+ repo: repo.fullName,
332
+ state: parsed.data.state,
333
+ fetched: issues.length,
334
+ created,
335
+ skipped,
336
+ })
337
+ }
@@ -1,8 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
3
- import { sendSol } from '@/lib/server/solana'
4
3
  import { notify } from '@/lib/server/ws-hub'
5
4
  import type { AgentWallet, WalletTransaction } from '@/types'
5
+ import { getWalletAtomicAmount } from '@/lib/wallet'
6
+ import { sendWalletNativeAsset, validateWalletSendLimits } from '@/lib/server/wallet-service'
6
7
  export const dynamic = 'force-dynamic'
7
8
 
8
9
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -41,10 +42,23 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
41
42
 
42
43
  // Approve — sign and submit
43
44
  try {
44
- const { signature, fee } = await sendSol(wallet.encryptedPrivateKey, tx.toAddress, tx.amountLamports)
45
+ const limitError = validateWalletSendLimits({ wallet, amountAtomic: getWalletAtomicAmount(tx), excludeTransactionId: transactionId })
46
+ if (limitError) {
47
+ tx.status = 'failed'
48
+ upsertWalletTransaction(transactionId, tx)
49
+ notify('wallets')
50
+ return NextResponse.json({
51
+ error: limitError,
52
+ transactionId,
53
+ status: 'failed',
54
+ }, { status: limitError === 'Amount must be positive' ? 400 : 403 })
55
+ }
56
+
57
+ const { signature, feeAtomic } = await sendWalletNativeAsset(wallet, tx.toAddress, getWalletAtomicAmount(tx))
45
58
  tx.status = 'confirmed'
46
59
  tx.signature = signature
47
- tx.feeLamports = fee
60
+ tx.feeAtomic = feeAtomic
61
+ tx.feeLamports = wallet.chain === 'solana' && feeAtomic ? Number.parseInt(feeAtomic, 10) : undefined
48
62
  tx.approvedBy = 'user'
49
63
  upsertWalletTransaction(transactionId, tx)
50
64
  notify('wallets')
@@ -1,12 +1,46 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadWallets, upsertWallet, deleteWallet as deleteWalletFromStore, loadAgents, saveAgents } from '@/lib/server/storage'
3
- import { getBalance, lamportsToSol } from '@/lib/server/solana'
4
3
  import { notify } from '@/lib/server/ws-hub'
5
- import type { AgentWallet } from '@/types'
4
+ import { getWalletLimitAtomic, normalizeAtomicString } from '@/lib/wallet'
5
+ import type { AgentWallet, WalletAssetBalance, WalletPortfolioSummary } from '@/types'
6
+ import { buildEmptyWalletPortfolio } from '@/lib/server/wallet-portfolio'
7
+ import {
8
+ getAgentActiveWalletId,
9
+ getWalletPortfolioSnapshot,
10
+ linkWalletToAgent,
11
+ setAgentActiveWallet,
12
+ stripWalletPrivateKey,
13
+ unlinkWalletFromAgent,
14
+ } from '@/lib/server/wallet-service'
6
15
  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'))
16
+ const WALLET_DETAIL_PORTFOLIO_TIMEOUT_MS = 2500
17
+
18
+ function withPortfolio(
19
+ wallet: AgentWallet,
20
+ portfolio: {
21
+ balanceAtomic: string
22
+ balanceFormatted: string
23
+ balanceSymbol: string
24
+ balanceDisplay: string
25
+ balanceLamports?: number
26
+ balanceSol?: number
27
+ assets: WalletAssetBalance[]
28
+ summary: WalletPortfolioSummary
29
+ },
30
+ isActive: boolean,
31
+ ) {
32
+ return {
33
+ ...stripWalletPrivateKey(wallet as unknown as Record<string, unknown>),
34
+ balanceAtomic: portfolio.balanceAtomic,
35
+ balanceFormatted: portfolio.balanceFormatted,
36
+ balanceSymbol: portfolio.balanceSymbol,
37
+ balanceDisplay: portfolio.balanceDisplay,
38
+ balanceLamports: portfolio.balanceLamports,
39
+ balanceSol: portfolio.balanceSol,
40
+ assets: portfolio.assets,
41
+ portfolioSummary: portfolio.summary,
42
+ isActive,
43
+ }
10
44
  }
11
45
 
12
46
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -15,21 +49,19 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
15
49
  const wallet = wallets[id]
16
50
  if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
17
51
 
18
- // Fetch live on-chain balance
19
- let balanceLamports = 0
20
- let balanceSol = 0
52
+ let portfolio = buildEmptyWalletPortfolio(wallet)
21
53
  try {
22
- balanceLamports = await getBalance(wallet.publicKey)
23
- balanceSol = lamportsToSol(balanceLamports)
54
+ portfolio = await getWalletPortfolioSnapshot(wallet, {
55
+ timeoutMs: WALLET_DETAIL_PORTFOLIO_TIMEOUT_MS,
56
+ allowStale: true,
57
+ })
24
58
  } catch {
25
59
  // RPC failure — return 0
26
60
  }
27
61
 
28
- return NextResponse.json({
29
- ...stripPrivateKey(wallet as unknown as Record<string, unknown>),
30
- balanceLamports,
31
- balanceSol,
32
- })
62
+ const agents = loadAgents()
63
+ const isActive = getAgentActiveWalletId(agents[wallet.agentId]) === wallet.id
64
+ return NextResponse.json(withPortfolio(wallet, portfolio, isActive))
33
65
  }
34
66
 
35
67
  export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -39,6 +71,7 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
39
71
  if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
40
72
 
41
73
  const body = await req.json()
74
+ const shouldMakeActive = body.makeActive === true
42
75
 
43
76
  // Reassign wallet to a different agent
44
77
  if (typeof body.agentId === 'string' && body.agentId !== wallet.agentId) {
@@ -46,39 +79,51 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
46
79
  const newAgent = agents[body.agentId]
47
80
  if (!newAgent) return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
48
81
 
49
- // Check new agent doesn't already have a wallet
82
+ // Only one wallet per chain per agent.
50
83
  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 })
84
+ const conflict = Object.values(allWallets).find((w) => w.agentId === body.agentId && w.id !== id && w.chain === wallet.chain)
85
+ if (conflict) return NextResponse.json({ error: `Target agent already has a ${wallet.chain} wallet` }, { status: 409 })
53
86
 
54
- // Unlink old agent
55
87
  const oldAgent = agents[wallet.agentId]
56
88
  if (oldAgent) {
57
- oldAgent.walletId = null
89
+ unlinkWalletFromAgent(oldAgent, id)
58
90
  oldAgent.updatedAt = Date.now()
59
91
  agents[wallet.agentId] = oldAgent
60
92
  }
61
93
 
62
- // Link new agent
63
- newAgent.walletId = id
94
+ linkWalletToAgent(newAgent, id, shouldMakeActive || getAgentActiveWalletId(newAgent) == null)
64
95
  newAgent.updatedAt = Date.now()
65
96
  agents[body.agentId] = newAgent
66
97
  saveAgents(agents)
67
98
  notify('agents')
68
99
 
69
100
  wallet.agentId = body.agentId
101
+ } else if (shouldMakeActive) {
102
+ const agents = loadAgents()
103
+ const agent = agents[wallet.agentId]
104
+ if (agent) {
105
+ setAgentActiveWallet(agent, id)
106
+ agent.updatedAt = Date.now()
107
+ agents[wallet.agentId] = agent
108
+ saveAgents(agents)
109
+ notify('agents')
110
+ }
70
111
  }
71
112
 
72
113
  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
114
+ if (body.spendingLimitAtomic !== undefined || body.spendingLimitLamports !== undefined) {
115
+ wallet.spendingLimitAtomic = normalizeAtomicString(body.spendingLimitAtomic ?? body.spendingLimitLamports, getWalletLimitAtomic(wallet, 'perTx'))
116
+ }
117
+ if (body.dailyLimitAtomic !== undefined || body.dailyLimitLamports !== undefined) {
118
+ wallet.dailyLimitAtomic = normalizeAtomicString(body.dailyLimitAtomic ?? body.dailyLimitLamports, getWalletLimitAtomic(wallet, 'daily'))
119
+ }
75
120
  if (typeof body.requireApproval === 'boolean') wallet.requireApproval = body.requireApproval
76
121
  wallet.updatedAt = Date.now()
77
122
 
78
123
  upsertWallet(id, wallet)
79
124
  notify('wallets')
80
125
 
81
- return NextResponse.json(stripPrivateKey(wallet as unknown as Record<string, unknown>))
126
+ return NextResponse.json(stripWalletPrivateKey(wallet as unknown as Record<string, unknown>))
82
127
  }
83
128
 
84
129
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -88,20 +133,19 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
88
133
  if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
89
134
 
90
135
  // Check if balance > 0 and warn
91
- let balanceLamports = 0
136
+ let portfolio = buildEmptyWalletPortfolio(wallet)
92
137
  try {
93
- balanceLamports = await getBalance(wallet.publicKey)
138
+ portfolio = await getWalletPortfolioSnapshot(wallet, {
139
+ timeoutMs: WALLET_DETAIL_PORTFOLIO_TIMEOUT_MS,
140
+ allowStale: true,
141
+ })
94
142
  } catch { /* ignore */ }
95
143
 
96
- if (balanceLamports > 0) {
97
- // Still delete, but include warning
98
- }
99
-
100
144
  // Unlink from agent
101
145
  const agents = loadAgents()
102
146
  const agent = agents[wallet.agentId]
103
147
  if (agent) {
104
- agent.walletId = null
148
+ unlinkWalletFromAgent(agent, id)
105
149
  agent.updatedAt = Date.now()
106
150
  agents[wallet.agentId] = agent
107
151
  saveAgents(agents)
@@ -113,6 +157,8 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
113
157
 
114
158
  return NextResponse.json({
115
159
  ok: true,
116
- warning: balanceLamports > 0 ? `Wallet had ${lamportsToSol(balanceLamports)} SOL remaining` : undefined,
160
+ warning: portfolio.summary.nonZeroAssets > 0
161
+ ? `Wallet still had ${portfolio.summary.nonZeroAssets} asset${portfolio.summary.nonZeroAssets === 1 ? '' : 's'} remaining, including ${portfolio.balanceDisplay}`
162
+ : undefined,
117
163
  })
118
164
  }
@@ -1,9 +1,12 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
- import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
4
- import { sendSol, isValidSolanaAddress, lamportsToSol } from '@/lib/server/solana'
3
+ import { loadWallets, upsertWalletTransaction } from '@/lib/server/storage'
5
4
  import { notify } from '@/lib/server/ws-hub'
6
5
  import type { AgentWallet, WalletTransaction } from '@/types'
6
+ import {
7
+ normalizeAtomicString,
8
+ } from '@/lib/wallet'
9
+ import { isValidWalletAddress, sendWalletNativeAsset, validateWalletSendLimits } from '@/lib/server/wallet-service'
7
10
  export const dynamic = 'force-dynamic'
8
11
 
9
12
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -14,36 +17,15 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
14
17
 
15
18
  const body = await req.json()
16
19
  const toAddress = typeof body.toAddress === 'string' ? body.toAddress.trim() : ''
17
- const amountLamports = typeof body.amountLamports === 'number' ? Math.floor(body.amountLamports) : 0
20
+ const amountAtomic = normalizeAtomicString(body.amountAtomic ?? body.amountLamports, '0')
18
21
  const memo = typeof body.memo === 'string' ? body.memo.slice(0, 500) : undefined
19
22
 
20
- if (!toAddress || !isValidSolanaAddress(toAddress)) {
23
+ if (!toAddress || !isValidWalletAddress(wallet.chain, toAddress)) {
21
24
  return NextResponse.json({ error: 'Invalid recipient address' }, { status: 400 })
22
25
  }
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 })
26
+ const limitError = validateWalletSendLimits({ wallet, amountAtomic })
27
+ if (limitError) {
28
+ return NextResponse.json({ error: limitError }, { status: limitError === 'Amount must be positive' ? 400 : 403 })
47
29
  }
48
30
 
49
31
  const txId = genId(8)
@@ -60,7 +42,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
60
42
  signature: '',
61
43
  fromAddress: wallet.publicKey,
62
44
  toAddress,
63
- amountLamports,
45
+ amountAtomic,
46
+ amountLamports: wallet.chain === 'solana' ? Number.parseInt(amountAtomic, 10) : undefined,
64
47
  status: 'pending_approval',
65
48
  memo,
66
49
  timestamp: now,
@@ -72,7 +55,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
72
55
 
73
56
  // Auto-approved — sign and submit
74
57
  try {
75
- const { signature, fee } = await sendSol(wallet.encryptedPrivateKey, toAddress, amountLamports)
58
+ const { signature, feeAtomic } = await sendWalletNativeAsset(wallet, toAddress, amountAtomic)
76
59
  const confirmedTx: WalletTransaction = {
77
60
  id: txId,
78
61
  walletId: id,
@@ -82,8 +65,10 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
82
65
  signature,
83
66
  fromAddress: wallet.publicKey,
84
67
  toAddress,
85
- amountLamports,
86
- feeLamports: fee,
68
+ amountAtomic,
69
+ amountLamports: wallet.chain === 'solana' ? Number.parseInt(amountAtomic, 10) : undefined,
70
+ feeAtomic,
71
+ feeLamports: wallet.chain === 'solana' && feeAtomic ? Number.parseInt(feeAtomic, 10) : undefined,
87
72
  status: 'confirmed',
88
73
  memo,
89
74
  approvedBy: 'auto',
@@ -102,7 +87,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
102
87
  signature: '',
103
88
  fromAddress: wallet.publicKey,
104
89
  toAddress,
105
- amountLamports,
90
+ amountAtomic,
91
+ amountLamports: wallet.chain === 'solana' ? Number.parseInt(amountAtomic, 10) : undefined,
106
92
  status: 'failed',
107
93
  memo,
108
94
  timestamp: now,