@swarmclawai/swarmclaw 1.2.8 → 1.3.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 (214) hide show
  1. package/README.md +39 -6
  2. package/package.json +2 -2
  3. package/src/app/agents/[id]/page.tsx +1 -18
  4. package/src/app/api/activity/route.ts +9 -23
  5. package/src/app/api/agents/route.ts +17 -1
  6. package/src/app/api/agents/thread-route.test.ts +0 -1
  7. package/src/app/api/approvals/route.test.ts +6 -22
  8. package/src/app/api/approvals/route.ts +13 -5
  9. package/src/app/api/connectors/route.ts +2 -2
  10. package/src/app/api/credentials/[id]/route.ts +2 -0
  11. package/src/app/api/credentials/route.ts +4 -1
  12. package/src/app/api/goals/[id]/route.ts +28 -0
  13. package/src/app/api/goals/route.ts +33 -0
  14. package/src/app/api/portability/export/route.ts +8 -0
  15. package/src/app/api/portability/import/route.test.ts +80 -0
  16. package/src/app/api/portability/import/route.ts +28 -0
  17. package/src/app/api/protocols/templates/[id]/route.ts +2 -1
  18. package/src/app/api/protocols/templates/route.ts +2 -1
  19. package/src/app/api/settings/route.ts +13 -2
  20. package/src/app/api/wallets/[id]/route.ts +15 -157
  21. package/src/app/api/wallets/generate/route.ts +22 -0
  22. package/src/app/api/wallets/route.test.ts +147 -0
  23. package/src/app/api/wallets/route.ts +13 -95
  24. package/src/app/autonomy/page.tsx +2 -57
  25. package/src/app/home/page.tsx +3 -0
  26. package/src/app/protocols/page.tsx +2 -21
  27. package/src/app/settings/page.tsx +0 -9
  28. package/src/app/wallets/page.tsx +105 -5
  29. package/src/cli/index.js +32 -33
  30. package/src/cli/spec.js +26 -27
  31. package/src/components/agents/agent-sheet.tsx +2 -40
  32. package/src/components/agents/inspector-panel.tsx +0 -83
  33. package/src/components/chat/chat-card.tsx +0 -31
  34. package/src/components/chat/message-bubble.tsx +1 -108
  35. package/src/components/connectors/connector-sheet.tsx +25 -1
  36. package/src/components/layout/sidebar-rail.tsx +6 -10
  37. package/src/components/projects/project-detail.tsx +3 -35
  38. package/src/components/projects/tabs/overview-tab.tsx +3 -59
  39. package/src/components/projects/tabs/work-tab.tsx +7 -77
  40. package/src/components/protocols/structured-session-launcher.tsx +1 -22
  41. package/src/components/shared/connector-platform-icon.tsx +1 -0
  42. package/src/components/tasks/task-card.tsx +4 -34
  43. package/src/components/tasks/task-sheet.tsx +6 -36
  44. package/src/components/wallets/wallet-list.tsx +150 -0
  45. package/src/lib/app/navigation.test.ts +0 -13
  46. package/src/lib/app/navigation.ts +2 -7
  47. package/src/lib/app/view-constants.ts +14 -19
  48. package/src/lib/server/activity/activity-log.ts +16 -1
  49. package/src/lib/server/agents/agent-service.ts +24 -11
  50. package/src/lib/server/agents/agent-thread-session.ts +0 -1
  51. package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
  52. package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
  53. package/src/lib/server/agents/delegation-jobs.ts +0 -25
  54. package/src/lib/server/agents/main-agent-loop.ts +1 -49
  55. package/src/lib/server/agents/subagent-runtime.ts +0 -1
  56. package/src/lib/server/approval-match.ts +14 -85
  57. package/src/lib/server/approvals/approval-hooks.ts +81 -0
  58. package/src/lib/server/approvals.test.ts +6 -6
  59. package/src/lib/server/approvals.ts +11 -6
  60. package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
  61. package/src/lib/server/builtin-extensions.ts +0 -2
  62. package/src/lib/server/capability-router.test.ts +0 -2
  63. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +14 -14
  64. package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
  65. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -2
  66. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
  67. package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
  68. package/src/lib/server/chat-execution/chat-turn-preparation.ts +2 -22
  69. package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
  70. package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
  71. package/src/lib/server/chat-execution/message-classifier.ts +1 -16
  72. package/src/lib/server/chat-execution/prompt-builder.test.ts +0 -1
  73. package/src/lib/server/chat-execution/prompt-builder.ts +0 -30
  74. package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
  75. package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
  76. package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
  77. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +8 -123
  78. package/src/lib/server/chat-execution/stream-agent-chat.ts +1 -5
  79. package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
  80. package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
  81. package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
  82. package/src/lib/server/chats/chat-session-service.ts +3 -5
  83. package/src/lib/server/connectors/connector-inbound.ts +0 -1
  84. package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
  85. package/src/lib/server/connectors/connector-service.ts +39 -9
  86. package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
  87. package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
  88. package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
  89. package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
  90. package/src/lib/server/connectors/swarmdock-tasks.ts +127 -0
  91. package/src/lib/server/connectors/swarmdock.ts +285 -0
  92. package/src/lib/server/execution-brief.test.ts +2 -25
  93. package/src/lib/server/execution-brief.ts +30 -35
  94. package/src/lib/server/execution-engine/task-attempt.ts +0 -1
  95. package/src/lib/server/goals/goal-repository.ts +19 -0
  96. package/src/lib/server/goals/goal-service.ts +143 -0
  97. package/src/lib/server/persistence/storage-context.ts +0 -5
  98. package/src/lib/server/portability/export.ts +109 -0
  99. package/src/lib/server/portability/import.ts +159 -0
  100. package/src/lib/server/protocols/protocol-normalization.ts +0 -4
  101. package/src/lib/server/protocols/protocol-queries.ts +0 -6
  102. package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
  103. package/src/lib/server/protocols/protocol-service.ts +0 -1
  104. package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
  105. package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
  106. package/src/lib/server/protocols/protocol-swarm.ts +0 -2
  107. package/src/lib/server/protocols/protocol-types.ts +0 -2
  108. package/src/lib/server/provider-health.ts +0 -9
  109. package/src/lib/server/runtime/daemon-state/core.ts +0 -9
  110. package/src/lib/server/runtime/daemon-state.test.ts +0 -35
  111. package/src/lib/server/runtime/heartbeat-service.ts +3 -23
  112. package/src/lib/server/runtime/queue/core.ts +11 -33
  113. package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
  114. package/src/lib/server/runtime/scheduler.ts +0 -13
  115. package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
  116. package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
  117. package/src/lib/server/runtime/session-run-manager/queries.ts +0 -1
  118. package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
  119. package/src/lib/server/runtime/session-run-manager.test.ts +0 -28
  120. package/src/lib/server/session-tools/crud.ts +0 -14
  121. package/src/lib/server/session-tools/delegate.ts +0 -4
  122. package/src/lib/server/session-tools/index.ts +0 -4
  123. package/src/lib/server/session-tools/team-context.ts +0 -3
  124. package/src/lib/server/storage-normalization.ts +13 -0
  125. package/src/lib/server/storage.ts +75 -45
  126. package/src/lib/server/tasks/task-checkout.ts +59 -0
  127. package/src/lib/server/tasks/task-lifecycle.ts +2 -0
  128. package/src/lib/server/tasks/task-route-service.ts +4 -26
  129. package/src/lib/server/tasks/task-service.ts +0 -7
  130. package/src/lib/server/tool-aliases.ts +0 -1
  131. package/src/lib/server/tool-capability-policy-advanced.test.ts +4 -4
  132. package/src/lib/server/tool-capability-policy.ts +0 -2
  133. package/src/lib/server/tool-planning.ts +0 -12
  134. package/src/lib/server/universal-tool-access.ts +0 -1
  135. package/src/lib/server/usage/cost-rollup.ts +124 -0
  136. package/src/lib/server/usage/usage-repository.ts +6 -0
  137. package/src/lib/server/wallets/wallet-crypto.ts +33 -0
  138. package/src/lib/server/wallets/wallet-repository.ts +24 -0
  139. package/src/lib/server/wallets/wallet-service.ts +119 -0
  140. package/src/lib/server/working-state/extraction.ts +8 -42
  141. package/src/lib/server/working-state/normalization.ts +10 -103
  142. package/src/lib/server/working-state/service.ts +12 -21
  143. package/src/lib/strip-internal-metadata.test.ts +1 -1
  144. package/src/lib/strip-internal-metadata.ts +1 -1
  145. package/src/lib/tool-definitions.ts +0 -1
  146. package/src/lib/validation/schemas.ts +36 -32
  147. package/src/lib/validation/server-schemas.ts +35 -0
  148. package/src/stores/slices/data-slice.ts +5 -1
  149. package/src/stores/slices/ui-slice.ts +0 -4
  150. package/src/types/agent.ts +10 -84
  151. package/src/types/app-settings.ts +6 -2
  152. package/src/types/approval.ts +3 -2
  153. package/src/types/connector.ts +1 -0
  154. package/src/types/goal.ts +30 -0
  155. package/src/types/index.ts +2 -1
  156. package/src/types/message.ts +0 -1
  157. package/src/types/misc.ts +2 -4
  158. package/src/types/protocol.ts +0 -2
  159. package/src/types/run.ts +0 -3
  160. package/src/types/session.ts +1 -51
  161. package/src/types/swarmdock.ts +29 -0
  162. package/src/types/task.ts +9 -3
  163. package/src/types/working-state.ts +2 -9
  164. package/src/views/settings/section-runtime-loop.tsx +0 -14
  165. package/src/app/api/canvas/[sessionId]/route.ts +0 -35
  166. package/src/app/api/missions/[id]/actions/route.ts +0 -31
  167. package/src/app/api/missions/[id]/events/route.ts +0 -14
  168. package/src/app/api/missions/[id]/route.ts +0 -10
  169. package/src/app/api/missions/route.test.ts +0 -244
  170. package/src/app/api/missions/route.ts +0 -57
  171. package/src/app/api/wallets/[id]/approve/route.ts +0 -79
  172. package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
  173. package/src/app/api/wallets/[id]/send/route.ts +0 -113
  174. package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
  175. package/src/app/missions/[id]/page.tsx +0 -3
  176. package/src/app/missions/page.tsx +0 -685
  177. package/src/components/canvas/canvas-panel.tsx +0 -267
  178. package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
  179. package/src/components/wallets/wallet-panel.tsx +0 -1010
  180. package/src/components/wallets/wallet-section.tsx +0 -260
  181. package/src/features/missions/queries.ts +0 -23
  182. package/src/lib/canvas-content.test.ts +0 -360
  183. package/src/lib/canvas-content.ts +0 -198
  184. package/src/lib/server/canvas-content.test.ts +0 -32
  185. package/src/lib/server/canvas-content.ts +0 -6
  186. package/src/lib/server/ethereum.ts +0 -591
  187. package/src/lib/server/evm-swap.ts +0 -476
  188. package/src/lib/server/missions/mission-intent.test.ts +0 -63
  189. package/src/lib/server/missions/mission-intent.ts +0 -569
  190. package/src/lib/server/missions/mission-repository.ts +0 -74
  191. package/src/lib/server/missions/mission-service/actions.ts +0 -6
  192. package/src/lib/server/missions/mission-service/bindings.ts +0 -9
  193. package/src/lib/server/missions/mission-service/context.ts +0 -4
  194. package/src/lib/server/missions/mission-service/core.ts +0 -2271
  195. package/src/lib/server/missions/mission-service/queries.ts +0 -12
  196. package/src/lib/server/missions/mission-service/recovery.ts +0 -5
  197. package/src/lib/server/missions/mission-service/ticks.ts +0 -9
  198. package/src/lib/server/missions/mission-service.test.ts +0 -888
  199. package/src/lib/server/missions/mission-service.ts +0 -6
  200. package/src/lib/server/session-tools/canvas.ts +0 -105
  201. package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
  202. package/src/lib/server/session-tools/wallet.ts +0 -1287
  203. package/src/lib/server/solana.ts +0 -327
  204. package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
  205. package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
  206. package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
  207. package/src/lib/server/wallet/wallet-service.test.ts +0 -81
  208. package/src/lib/server/wallet/wallet-service.ts +0 -225
  209. package/src/lib/wallet/wallet-transactions.test.ts +0 -75
  210. package/src/lib/wallet/wallet-transactions.ts +0 -43
  211. package/src/lib/wallet/wallet.test.ts +0 -333
  212. package/src/lib/wallet/wallet.ts +0 -183
  213. package/src/types/mission.ts +0 -185
  214. package/src/views/settings/section-wallets.tsx +0 -35
@@ -1,476 +0,0 @@
1
- import { Contract, JsonRpcProvider, getAddress, isAddress } from 'ethers'
2
-
3
- import { formatAtomicAmount, normalizeAtomicString, parseDisplayAmountToAtomic } from '@/lib/wallet/wallet'
4
- import type { AgentWallet, WalletAssetBalance } from '@/types'
5
-
6
- import { getEvmNetworkConfig, getProviderForNetwork, type EvmNetworkId } from './ethereum'
7
- import { getWalletPortfolioSnapshot } from '@/lib/server/wallet/wallet-service'
8
- import { errorMessage } from '@/lib/shared-utils'
9
-
10
- const PARASWAP_API_BASE = 'https://api.paraswap.io'
11
- const PARASWAP_VERSION = '6.2'
12
- const PARASWAP_NATIVE_TOKEN = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
13
- const ERC20_ALLOWANCE_ABI = [
14
- 'function allowance(address owner, address spender) view returns (uint256)',
15
- 'function balanceOf(address owner) view returns (uint256)',
16
- 'function approve(address spender, uint256 amount) returns (bool)',
17
- 'function decimals() view returns (uint8)',
18
- 'function symbol() view returns (string)',
19
- 'function name() view returns (string)',
20
- ] as const
21
- const TOKEN_LIST_TTL_MS = 10 * 60 * 1000
22
- const FETCH_TIMEOUT_MS = 15_000
23
-
24
- interface CachedTokenList {
25
- expiresAt: number
26
- assets: ResolvedEvmSwapAsset[]
27
- }
28
-
29
- const paraswapTokenListCache = new Map<EvmNetworkId, CachedTokenList>()
30
-
31
- export interface ResolvedEvmSwapAsset {
32
- address: string
33
- symbol: string
34
- name: string
35
- decimals: number
36
- isNative: boolean
37
- source: 'native' | 'portfolio' | 'paraswap' | 'onchain'
38
- }
39
-
40
- export interface PreparedEvmSwapPlan {
41
- provider: 'paraswap'
42
- network: ReturnType<typeof getEvmNetworkConfig>
43
- walletAddress: string
44
- recipient: string
45
- sellToken: ResolvedEvmSwapAsset
46
- buyToken: ResolvedEvmSwapAsset
47
- sellAmountAtomic: string
48
- sellAmountDisplay: string
49
- buyAmountAtomic: string
50
- buyAmountDisplay: string
51
- slippageBps: number
52
- spenderAddress: string | null
53
- approvalRequired: boolean
54
- approvalTransaction: Record<string, unknown> | null
55
- swapTransaction: Record<string, unknown>
56
- routeSummary: string
57
- priceRoute: Record<string, unknown>
58
- }
59
-
60
- export interface PrepareEvmSwapPlanInput {
61
- wallet: AgentWallet
62
- network: EvmNetworkId | string
63
- sellToken: unknown
64
- buyToken: unknown
65
- sellAmountAtomic?: unknown
66
- sellAmountDisplay?: unknown
67
- slippageBps?: unknown
68
- recipient?: unknown
69
- rpcUrl?: string | null
70
- skipBalanceCheck?: boolean
71
- }
72
-
73
- function normalizeText(value: unknown): string {
74
- return typeof value === 'string' ? value.trim() : ''
75
- }
76
-
77
- function looksLikeEvmAddress(value: string): boolean {
78
- return isAddress(value)
79
- }
80
-
81
- function normalizeLowerAddress(value: string): string {
82
- return getAddress(value).toLowerCase()
83
- }
84
-
85
- function makeNativeEthAsset(): ResolvedEvmSwapAsset {
86
- return {
87
- address: PARASWAP_NATIVE_TOKEN,
88
- symbol: 'ETH',
89
- name: 'Ether',
90
- decimals: 18,
91
- isNative: true,
92
- source: 'native',
93
- }
94
- }
95
-
96
- function normalizeSlippageBps(value: unknown): number {
97
- if (typeof value === 'number' && Number.isFinite(value)) {
98
- if (value > 0 && value <= 10) return Math.round(value * 100)
99
- return Math.max(1, Math.min(5_000, Math.trunc(value)))
100
- }
101
- if (typeof value === 'string') {
102
- const trimmed = value.trim()
103
- if (!trimmed) return 100
104
- if (/^\d+(\.\d+)?$/.test(trimmed)) {
105
- const parsed = Number.parseFloat(trimmed)
106
- if (parsed > 0 && parsed <= 10) return Math.round(parsed * 100)
107
- return Math.max(1, Math.min(5_000, Math.trunc(parsed)))
108
- }
109
- }
110
- return 100
111
- }
112
-
113
- async function fetchJson(url: string, init?: RequestInit): Promise<unknown> {
114
- const controller = new AbortController()
115
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
116
- try {
117
- const response = await fetch(url, {
118
- ...init,
119
- signal: controller.signal,
120
- headers: {
121
- Accept: 'application/json',
122
- ...(init?.headers || {}),
123
- },
124
- })
125
- const text = await response.text()
126
- const payload = text ? JSON.parse(text) as unknown : null
127
- if (!response.ok) {
128
- const message = payload && typeof payload === 'object' && payload && 'error' in payload
129
- ? String((payload as { error?: unknown }).error)
130
- : `${response.status} ${response.statusText}`.trim()
131
- throw new Error(`ParaSwap API request failed: ${message}`)
132
- }
133
- return payload
134
- } finally {
135
- clearTimeout(timer)
136
- }
137
- }
138
-
139
- async function getParaswapTokenList(network: EvmNetworkId): Promise<ResolvedEvmSwapAsset[]> {
140
- const cached = paraswapTokenListCache.get(network)
141
- if (cached && cached.expiresAt > Date.now()) return cached.assets
142
-
143
- const config = getEvmNetworkConfig(network)
144
- const response = await fetchJson(`${PARASWAP_API_BASE}/tokens/${config.chainId}`) as {
145
- tokens?: Array<{
146
- address?: string
147
- symbol?: string
148
- name?: string
149
- decimals?: number
150
- }>
151
- }
152
- const assets = (Array.isArray(response?.tokens) ? response.tokens : [])
153
- .flatMap((token) => {
154
- const address = normalizeText(token?.address)
155
- const symbol = normalizeText(token?.symbol)
156
- if (!address || !symbol) return []
157
- if (address.toLowerCase() === PARASWAP_NATIVE_TOKEN.toLowerCase()) return [makeNativeEthAsset()]
158
- if (!looksLikeEvmAddress(address)) return []
159
- return [{
160
- address: getAddress(address),
161
- symbol,
162
- name: normalizeText(token?.name) || symbol,
163
- decimals: typeof token?.decimals === 'number' ? token.decimals : 18,
164
- isNative: false,
165
- source: 'paraswap' as const,
166
- }]
167
- })
168
-
169
- const deduped = Array.from(new Map(
170
- assets.map((asset) => [asset.address.toLowerCase(), asset]),
171
- ).values())
172
- paraswapTokenListCache.set(network, {
173
- expiresAt: Date.now() + TOKEN_LIST_TTL_MS,
174
- assets: deduped,
175
- })
176
- return deduped
177
- }
178
-
179
- function getPortfolioAssetCandidates(
180
- walletAssets: WalletAssetBalance[],
181
- networkId: EvmNetworkId,
182
- tokenRef: string,
183
- ): ResolvedEvmSwapAsset[] {
184
- const normalized = tokenRef.trim().toLowerCase()
185
- return walletAssets
186
- .filter((asset) => asset.chain === 'ethereum' && asset.networkId === networkId)
187
- .flatMap((asset) => {
188
- const address = asset.isNative ? PARASWAP_NATIVE_TOKEN : normalizeText(asset.contractAddress)
189
- if (!address) return []
190
- const symbol = normalizeText(asset.symbol)
191
- const name = normalizeText(asset.name)
192
- const matchesAddress = !asset.isNative && looksLikeEvmAddress(tokenRef) && address.toLowerCase() === normalized
193
- const matchesSymbol = symbol.toLowerCase() === normalized
194
- const matchesName = name.toLowerCase() === normalized
195
- const matchesNative = asset.isNative && ['eth', 'native', PARASWAP_NATIVE_TOKEN.toLowerCase()].includes(normalized)
196
- if (!matchesAddress && !matchesSymbol && !matchesName && !matchesNative) return []
197
- return [{
198
- address: asset.isNative ? PARASWAP_NATIVE_TOKEN : getAddress(address),
199
- symbol: symbol || (asset.isNative ? 'ETH' : 'TOKEN'),
200
- name: name || symbol || 'Token',
201
- decimals: typeof asset.decimals === 'number' ? asset.decimals : (asset.isNative ? 18 : 18),
202
- isNative: asset.isNative === true,
203
- source: 'portfolio' as const,
204
- }]
205
- })
206
- }
207
-
208
- async function resolveTokenByAddress(
209
- provider: JsonRpcProvider,
210
- address: string,
211
- ): Promise<ResolvedEvmSwapAsset> {
212
- const normalizedAddress = getAddress(address)
213
- const contract = new Contract(normalizedAddress, ERC20_ALLOWANCE_ABI, provider)
214
- const [decimalsRaw, symbolRaw, nameRaw] = await Promise.all([
215
- contract.decimals().catch(() => 18),
216
- contract.symbol().catch(() => 'TOKEN'),
217
- contract.name().catch(() => 'Token'),
218
- ])
219
- return {
220
- address: normalizedAddress,
221
- symbol: normalizeText(symbolRaw) || 'TOKEN',
222
- name: normalizeText(nameRaw) || normalizeText(symbolRaw) || 'Token',
223
- decimals: typeof decimalsRaw === 'number' ? decimalsRaw : Number(decimalsRaw ?? 18),
224
- isNative: false,
225
- source: 'onchain',
226
- }
227
- }
228
-
229
- export async function resolveEvmSwapAsset(input: {
230
- wallet: AgentWallet
231
- network: EvmNetworkId | string
232
- token: unknown
233
- rpcUrl?: string | null
234
- }): Promise<ResolvedEvmSwapAsset> {
235
- const tokenRef = normalizeText(input.token)
236
- if (!tokenRef) throw new Error('Token is required')
237
-
238
- const network = getEvmNetworkConfig(input.network).id
239
- const normalized = tokenRef.toLowerCase()
240
- if (['eth', 'native', PARASWAP_NATIVE_TOKEN.toLowerCase()].includes(normalized)) {
241
- return makeNativeEthAsset()
242
- }
243
-
244
- const portfolio = await getWalletPortfolioSnapshot(input.wallet)
245
- const portfolioMatches = getPortfolioAssetCandidates(portfolio.assets, network, tokenRef)
246
- if (portfolioMatches.length === 1) return portfolioMatches[0]
247
-
248
- const tokenList = await getParaswapTokenList(network)
249
- if (looksLikeEvmAddress(tokenRef)) {
250
- const addressMatch = tokenList.find((asset) => asset.address.toLowerCase() === normalized.toLowerCase())
251
- if (addressMatch) return addressMatch
252
- return resolveTokenByAddress(getProviderForNetwork(network, input.rpcUrl), tokenRef)
253
- }
254
-
255
- const symbolMatches = tokenList.filter((asset) => asset.symbol.toLowerCase() === normalized)
256
- if (symbolMatches.length === 1) return symbolMatches[0]
257
- if (portfolioMatches.length > 1) {
258
- throw new Error(`Token "${tokenRef}" matches multiple wallet assets on ${network}. Use the contract address instead.`)
259
- }
260
- if (symbolMatches.length > 1) {
261
- throw new Error(`Token "${tokenRef}" matches multiple ParaSwap assets on ${network}. Use the token contract address instead.`)
262
- }
263
-
264
- const nameMatch = tokenList.find((asset) => asset.name.toLowerCase() === normalized)
265
- if (nameMatch) return nameMatch
266
-
267
- throw new Error(`Could not resolve token "${tokenRef}" on ${network}. Use a symbol like USDC/ETH or a token contract address.`)
268
- }
269
-
270
- function parseSellAmountAtomic(input: {
271
- sellAmountAtomic?: unknown
272
- sellAmountDisplay?: unknown
273
- decimals: number
274
- }): string {
275
- const atomic = normalizeAtomicString(input.sellAmountAtomic, '')
276
- if (atomic) {
277
- if (BigInt(atomic) <= BigInt(0)) throw new Error('Swap amount must be positive')
278
- return atomic
279
- }
280
- const displayRaw = input.sellAmountDisplay
281
- if (
282
- displayRaw === undefined
283
- || displayRaw === null
284
- || (typeof displayRaw === 'string' && displayRaw.trim() === '')
285
- ) {
286
- throw new Error('sellAmountAtomic or sellAmountDisplay is required for swap')
287
- }
288
- const display = typeof displayRaw === 'number' || typeof displayRaw === 'string'
289
- ? displayRaw
290
- : String(displayRaw)
291
- const parsed = parseDisplayAmountToAtomic(display, input.decimals)
292
- if (BigInt(parsed) <= BigInt(0)) throw new Error('Swap amount must be positive')
293
- return parsed
294
- }
295
-
296
- async function getTokenBalanceAtomic(
297
- provider: JsonRpcProvider,
298
- walletAddress: string,
299
- token: ResolvedEvmSwapAsset,
300
- ): Promise<bigint> {
301
- if (token.isNative) {
302
- return provider.getBalance(walletAddress)
303
- }
304
- const contract = new Contract(token.address, ERC20_ALLOWANCE_ABI, provider)
305
- const balance = await contract.balanceOf(walletAddress)
306
- return BigInt(balance.toString())
307
- }
308
-
309
- async function getTokenAllowanceAtomic(
310
- provider: JsonRpcProvider,
311
- walletAddress: string,
312
- spenderAddress: string,
313
- token: ResolvedEvmSwapAsset,
314
- ): Promise<bigint> {
315
- if (token.isNative) return BigInt(0)
316
- const contract = new Contract(token.address, ERC20_ALLOWANCE_ABI, provider)
317
- const allowance = await contract.allowance(walletAddress, spenderAddress)
318
- return BigInt(allowance.toString())
319
- }
320
-
321
- function collectRouteExchanges(priceRoute: Record<string, unknown>): string[] {
322
- const bestRoute = Array.isArray(priceRoute.bestRoute) ? priceRoute.bestRoute : []
323
- const exchanges = new Set<string>()
324
- for (const route of bestRoute) {
325
- const swaps = Array.isArray((route as { swaps?: unknown[] }).swaps) ? (route as { swaps: unknown[] }).swaps : []
326
- for (const swap of swaps) {
327
- const swapExchanges = Array.isArray((swap as { swapExchanges?: unknown[] }).swapExchanges)
328
- ? (swap as { swapExchanges: unknown[] }).swapExchanges
329
- : []
330
- for (const entry of swapExchanges) {
331
- const exchange = normalizeText((entry as { exchange?: unknown }).exchange)
332
- if (exchange) exchanges.add(exchange)
333
- }
334
- }
335
- }
336
- return [...exchanges]
337
- }
338
-
339
- function toComparableTransaction(transaction: Record<string, unknown>, network: ReturnType<typeof getEvmNetworkConfig>): Record<string, unknown> {
340
- const normalized: Record<string, unknown> = {}
341
- const to = normalizeText(transaction.to)
342
- const data = normalizeText(transaction.data)
343
- const value = transaction.value
344
- if (to) normalized.to = getAddress(to)
345
- if (data) normalized.data = data
346
- if (value !== undefined && value !== null && String(value).trim() !== '') normalized.value = String(value).trim()
347
- normalized.chainId = network.chainId
348
- return normalized
349
- }
350
-
351
- export async function prepareEvmSwapPlan(input: PrepareEvmSwapPlanInput): Promise<PreparedEvmSwapPlan> {
352
- if (input.wallet.chain !== 'ethereum') {
353
- throw new Error('Generic swap is currently supported only for Ethereum-compatible wallets')
354
- }
355
-
356
- const network = getEvmNetworkConfig(input.network)
357
- const provider = getProviderForNetwork(network.id, input.rpcUrl || undefined)
358
- const walletAddress = getAddress(input.wallet.publicKey)
359
- const recipient = normalizeText(input.recipient) ? getAddress(normalizeText(input.recipient)) : walletAddress
360
- const sellToken = await resolveEvmSwapAsset({
361
- wallet: input.wallet,
362
- network: network.id,
363
- token: input.sellToken,
364
- rpcUrl: input.rpcUrl,
365
- })
366
- const buyToken = await resolveEvmSwapAsset({
367
- wallet: input.wallet,
368
- network: network.id,
369
- token: input.buyToken,
370
- rpcUrl: input.rpcUrl,
371
- })
372
- if (sellToken.address.toLowerCase() === buyToken.address.toLowerCase()) {
373
- throw new Error('Swap sellToken and buyToken must be different')
374
- }
375
-
376
- const sellAmountAtomic = parseSellAmountAtomic({
377
- sellAmountAtomic: input.sellAmountAtomic,
378
- sellAmountDisplay: input.sellAmountDisplay,
379
- decimals: sellToken.decimals,
380
- })
381
- if (input.skipBalanceCheck !== true) {
382
- const sellBalanceAtomic = await getTokenBalanceAtomic(provider, walletAddress, sellToken)
383
- if (sellBalanceAtomic < BigInt(sellAmountAtomic)) {
384
- const available = formatAtomicAmount(sellBalanceAtomic.toString(), sellToken.decimals, { maxFractionDigits: 6 })
385
- throw new Error(`Insufficient ${sellToken.symbol} balance on ${network.label}. Available ${available} ${sellToken.symbol}.`)
386
- }
387
- }
388
-
389
- const priceUrl = new URL(`${PARASWAP_API_BASE}/prices`)
390
- priceUrl.searchParams.set('srcToken', sellToken.address)
391
- priceUrl.searchParams.set('destToken', buyToken.address)
392
- priceUrl.searchParams.set('amount', sellAmountAtomic)
393
- priceUrl.searchParams.set('srcDecimals', String(sellToken.decimals))
394
- priceUrl.searchParams.set('destDecimals', String(buyToken.decimals))
395
- priceUrl.searchParams.set('side', 'SELL')
396
- priceUrl.searchParams.set('network', String(network.chainId))
397
- priceUrl.searchParams.set('version', PARASWAP_VERSION)
398
- const priceResponse = await fetchJson(priceUrl.toString()) as { priceRoute?: Record<string, unknown> }
399
- const priceRoute = priceResponse?.priceRoute
400
- if (!priceRoute || typeof priceRoute !== 'object') {
401
- throw new Error('ParaSwap did not return a price route')
402
- }
403
-
404
- const transactionsUrl = `${PARASWAP_API_BASE}/transactions/${network.chainId}?ignoreChecks=true`
405
- const transactionsRequest = {
406
- srcToken: sellToken.address,
407
- destToken: buyToken.address,
408
- srcAmount: sellAmountAtomic,
409
- userAddress: walletAddress,
410
- srcDecimals: sellToken.decimals,
411
- destDecimals: buyToken.decimals,
412
- priceRoute,
413
- receiver: recipient,
414
- slippage: normalizeSlippageBps(input.slippageBps),
415
- }
416
- const swapResponse = await fetchJson(transactionsUrl, {
417
- method: 'POST',
418
- headers: { 'Content-Type': 'application/json' },
419
- body: JSON.stringify(transactionsRequest),
420
- }) as Record<string, unknown>
421
-
422
- const rawTo = normalizeText(swapResponse.to)
423
- const rawData = normalizeText(swapResponse.data)
424
- if (!rawTo || !rawData) {
425
- throw new Error('ParaSwap did not return executable transaction calldata')
426
- }
427
-
428
- const spenderAddress = normalizeText((priceRoute as { tokenTransferProxy?: unknown }).tokenTransferProxy)
429
- || normalizeText((priceRoute as { contractAddress?: unknown }).contractAddress)
430
- || rawTo
431
-
432
- let approvalRequired = false
433
- let approvalTransaction: Record<string, unknown> | null = null
434
- if (!sellToken.isNative) {
435
- const allowance = await getTokenAllowanceAtomic(provider, walletAddress, getAddress(spenderAddress), sellToken)
436
- approvalRequired = allowance < BigInt(sellAmountAtomic)
437
- if (approvalRequired) {
438
- approvalTransaction = {
439
- to: getAddress(sellToken.address),
440
- data: new Contract(sellToken.address, ERC20_ALLOWANCE_ABI, provider).interface.encodeFunctionData('approve', [
441
- getAddress(spenderAddress),
442
- BigInt(sellAmountAtomic),
443
- ]),
444
- value: '0',
445
- chainId: network.chainId,
446
- }
447
- }
448
- }
449
-
450
- const buyAmountAtomic = normalizeAtomicString((priceRoute as { destAmount?: unknown }).destAmount, '0')
451
- const exchanges = collectRouteExchanges(priceRoute)
452
- return {
453
- provider: 'paraswap',
454
- network,
455
- walletAddress,
456
- recipient,
457
- sellToken,
458
- buyToken,
459
- sellAmountAtomic,
460
- sellAmountDisplay: `${formatAtomicAmount(sellAmountAtomic, sellToken.decimals, { maxFractionDigits: 6 })} ${sellToken.symbol}`,
461
- buyAmountAtomic,
462
- buyAmountDisplay: `${formatAtomicAmount(buyAmountAtomic, buyToken.decimals, { maxFractionDigits: 6 })} ${buyToken.symbol}`,
463
- slippageBps: normalizeSlippageBps(input.slippageBps),
464
- spenderAddress: spenderAddress ? getAddress(spenderAddress) : null,
465
- approvalRequired,
466
- approvalTransaction,
467
- swapTransaction: toComparableTransaction(swapResponse, network),
468
- routeSummary: exchanges.length > 0 ? exchanges.join(', ') : 'ParaSwap route',
469
- priceRoute,
470
- }
471
- }
472
-
473
- export function isLikelyRetryableSwapError(err: unknown): boolean {
474
- const message = errorMessage(err)
475
- return /rate|price|slippage|expired|call exception|execution reverted|insufficient output/i.test(message)
476
- }
@@ -1,63 +0,0 @@
1
- import assert from 'node:assert/strict'
2
- import { describe, it } from 'node:test'
3
-
4
- import {
5
- parseMissionOutcomeDecision,
6
- parseMissionPlannerDecision,
7
- parseMissionTurnDecision,
8
- } from '@/lib/server/missions/mission-intent'
9
-
10
- describe('mission-intent parsing', () => {
11
- it('parses mission turn decisions from structured JSON output', () => {
12
- const decision = parseMissionTurnDecision([
13
- 'Here is the result.',
14
- '{"action":"create_new","confidence":0.91,"objective":"Ship the release prep flow","successCriteria":["README updated","release verified"],"currentStep":"Audit the repo","plannerSummary":"Turn the release request into a tracked mission."}',
15
- ].join('\n'))
16
-
17
- assert.deepEqual(decision, {
18
- action: 'create_new',
19
- confidence: 0.91,
20
- objective: 'Ship the release prep flow',
21
- successCriteria: ['README updated', 'release verified'],
22
- currentStep: 'Audit the repo',
23
- plannerSummary: 'Turn the release request into a tracked mission.',
24
- })
25
- })
26
-
27
- it('returns null for malformed mission turn JSON instead of throwing', () => {
28
- assert.equal(parseMissionTurnDecision('{"action":"create_new",'), null)
29
- assert.equal(parseMissionTurnDecision('not json at all'), null)
30
- })
31
-
32
- it('parses mission outcome decisions from structured JSON output', () => {
33
- const decision = parseMissionOutcomeDecision([
34
- 'done',
35
- '{"verdict":"waiting","confidence":0.72,"phase":"waiting","currentStep":"Wait for approval","verifierSummary":"The mission is blocked on a human approval.","waitKind":"approval","waitReason":"Resume approval still pending."}',
36
- ].join('\n'))
37
-
38
- assert.deepEqual(decision, {
39
- verdict: 'waiting',
40
- confidence: 0.72,
41
- phase: 'waiting',
42
- currentStep: 'Wait for approval',
43
- verifierSummary: 'The mission is blocked on a human approval.',
44
- waitKind: 'approval',
45
- waitReason: 'Resume approval still pending.',
46
- })
47
- })
48
-
49
- it('parses mission planner decisions from structured JSON output', () => {
50
- const decision = parseMissionPlannerDecision([
51
- 'planner',
52
- '{"decision":"dispatch_session_turn","confidence":0.84,"summary":"Queue the next durable turn.","currentStep":"Summarize the release blockers","sessionMessage":"Continue the mission and summarize the remaining release blockers."}',
53
- ].join('\n'))
54
-
55
- assert.deepEqual(decision, {
56
- decision: 'dispatch_session_turn',
57
- confidence: 0.84,
58
- summary: 'Queue the next durable turn.',
59
- currentStep: 'Summarize the release blockers',
60
- sessionMessage: 'Continue the mission and summarize the remaining release blockers.',
61
- })
62
- })
63
- })