@swarmclawai/swarmclaw 1.2.8 → 1.2.9

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 (195) hide show
  1. package/README.md +30 -6
  2. package/package.json +2 -2
  3. package/src/app/agents/[id]/page.tsx +1 -18
  4. package/src/app/api/agents/thread-route.test.ts +0 -1
  5. package/src/app/api/approvals/route.test.ts +6 -22
  6. package/src/app/api/connectors/route.ts +2 -2
  7. package/src/app/api/portability/export/route.ts +8 -0
  8. package/src/app/api/portability/import/route.test.ts +80 -0
  9. package/src/app/api/portability/import/route.ts +28 -0
  10. package/src/app/api/settings/route.ts +0 -2
  11. package/src/app/api/wallets/[id]/route.ts +15 -157
  12. package/src/app/api/wallets/generate/route.ts +22 -0
  13. package/src/app/api/wallets/route.test.ts +147 -0
  14. package/src/app/api/wallets/route.ts +13 -95
  15. package/src/app/autonomy/page.tsx +2 -57
  16. package/src/app/protocols/page.tsx +2 -21
  17. package/src/app/settings/page.tsx +0 -9
  18. package/src/app/wallets/page.tsx +105 -5
  19. package/src/cli/index.js +21 -33
  20. package/src/cli/spec.js +19 -30
  21. package/src/components/agents/agent-sheet.tsx +2 -40
  22. package/src/components/agents/inspector-panel.tsx +0 -83
  23. package/src/components/chat/chat-card.tsx +0 -31
  24. package/src/components/chat/message-bubble.tsx +1 -108
  25. package/src/components/connectors/connector-sheet.tsx +25 -1
  26. package/src/components/layout/sidebar-rail.tsx +6 -10
  27. package/src/components/projects/project-detail.tsx +3 -35
  28. package/src/components/projects/tabs/overview-tab.tsx +3 -59
  29. package/src/components/projects/tabs/work-tab.tsx +7 -77
  30. package/src/components/protocols/structured-session-launcher.tsx +1 -22
  31. package/src/components/shared/connector-platform-icon.tsx +1 -0
  32. package/src/components/tasks/task-card.tsx +4 -34
  33. package/src/components/tasks/task-sheet.tsx +6 -36
  34. package/src/components/wallets/wallet-list.tsx +150 -0
  35. package/src/lib/app/navigation.test.ts +0 -13
  36. package/src/lib/app/navigation.ts +2 -7
  37. package/src/lib/app/view-constants.ts +14 -19
  38. package/src/lib/server/agents/agent-thread-session.ts +0 -1
  39. package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
  40. package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
  41. package/src/lib/server/agents/delegation-jobs.ts +0 -25
  42. package/src/lib/server/agents/main-agent-loop.ts +1 -49
  43. package/src/lib/server/agents/subagent-runtime.ts +0 -1
  44. package/src/lib/server/approval-match.ts +0 -85
  45. package/src/lib/server/approvals.test.ts +6 -6
  46. package/src/lib/server/approvals.ts +0 -6
  47. package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
  48. package/src/lib/server/builtin-extensions.ts +0 -2
  49. package/src/lib/server/capability-router.test.ts +0 -2
  50. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +14 -14
  51. package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
  52. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -2
  53. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
  54. package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
  55. package/src/lib/server/chat-execution/chat-turn-preparation.ts +2 -22
  56. package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
  57. package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
  58. package/src/lib/server/chat-execution/message-classifier.ts +1 -16
  59. package/src/lib/server/chat-execution/prompt-builder.test.ts +0 -1
  60. package/src/lib/server/chat-execution/prompt-builder.ts +0 -30
  61. package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
  62. package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
  63. package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
  64. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +8 -123
  65. package/src/lib/server/chat-execution/stream-agent-chat.ts +1 -5
  66. package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
  67. package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
  68. package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
  69. package/src/lib/server/chats/chat-session-service.ts +3 -5
  70. package/src/lib/server/connectors/connector-inbound.ts +0 -1
  71. package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
  72. package/src/lib/server/connectors/connector-service.ts +39 -9
  73. package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
  74. package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
  75. package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
  76. package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
  77. package/src/lib/server/connectors/swarmdock-tasks.ts +119 -0
  78. package/src/lib/server/connectors/swarmdock.ts +255 -0
  79. package/src/lib/server/execution-brief.test.ts +2 -25
  80. package/src/lib/server/execution-brief.ts +12 -35
  81. package/src/lib/server/execution-engine/task-attempt.ts +0 -1
  82. package/src/lib/server/persistence/storage-context.ts +0 -5
  83. package/src/lib/server/portability/export.ts +109 -0
  84. package/src/lib/server/portability/import.ts +159 -0
  85. package/src/lib/server/protocols/protocol-normalization.ts +0 -4
  86. package/src/lib/server/protocols/protocol-queries.ts +0 -6
  87. package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
  88. package/src/lib/server/protocols/protocol-service.ts +0 -1
  89. package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
  90. package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
  91. package/src/lib/server/protocols/protocol-swarm.ts +0 -2
  92. package/src/lib/server/protocols/protocol-types.ts +0 -2
  93. package/src/lib/server/provider-health.ts +0 -9
  94. package/src/lib/server/runtime/daemon-state/core.ts +0 -9
  95. package/src/lib/server/runtime/daemon-state.test.ts +0 -35
  96. package/src/lib/server/runtime/heartbeat-service.ts +3 -23
  97. package/src/lib/server/runtime/queue/core.ts +11 -33
  98. package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
  99. package/src/lib/server/runtime/scheduler.ts +0 -13
  100. package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
  101. package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
  102. package/src/lib/server/runtime/session-run-manager/queries.ts +0 -1
  103. package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
  104. package/src/lib/server/runtime/session-run-manager.test.ts +0 -28
  105. package/src/lib/server/session-tools/crud.ts +0 -14
  106. package/src/lib/server/session-tools/delegate.ts +0 -4
  107. package/src/lib/server/session-tools/index.ts +0 -4
  108. package/src/lib/server/session-tools/team-context.ts +0 -3
  109. package/src/lib/server/storage-normalization.ts +8 -0
  110. package/src/lib/server/storage.ts +18 -45
  111. package/src/lib/server/tasks/task-checkout.ts +59 -0
  112. package/src/lib/server/tasks/task-lifecycle.ts +2 -0
  113. package/src/lib/server/tasks/task-route-service.ts +4 -26
  114. package/src/lib/server/tasks/task-service.ts +0 -7
  115. package/src/lib/server/tool-aliases.ts +0 -1
  116. package/src/lib/server/tool-capability-policy-advanced.test.ts +4 -4
  117. package/src/lib/server/tool-capability-policy.ts +0 -2
  118. package/src/lib/server/tool-planning.ts +0 -12
  119. package/src/lib/server/universal-tool-access.ts +0 -1
  120. package/src/lib/server/wallets/wallet-crypto.ts +33 -0
  121. package/src/lib/server/wallets/wallet-repository.ts +24 -0
  122. package/src/lib/server/wallets/wallet-service.ts +119 -0
  123. package/src/lib/server/working-state/extraction.ts +8 -42
  124. package/src/lib/server/working-state/normalization.ts +10 -103
  125. package/src/lib/server/working-state/service.ts +12 -21
  126. package/src/lib/strip-internal-metadata.test.ts +1 -1
  127. package/src/lib/strip-internal-metadata.ts +1 -1
  128. package/src/lib/tool-definitions.ts +0 -1
  129. package/src/lib/validation/schemas.ts +33 -2
  130. package/src/stores/slices/data-slice.ts +5 -1
  131. package/src/stores/slices/ui-slice.ts +0 -4
  132. package/src/types/agent.ts +0 -84
  133. package/src/types/app-settings.ts +0 -2
  134. package/src/types/approval.ts +0 -2
  135. package/src/types/connector.ts +1 -0
  136. package/src/types/index.ts +1 -1
  137. package/src/types/message.ts +0 -1
  138. package/src/types/misc.ts +0 -2
  139. package/src/types/protocol.ts +0 -2
  140. package/src/types/run.ts +0 -3
  141. package/src/types/session.ts +1 -51
  142. package/src/types/swarmdock.ts +29 -0
  143. package/src/types/task.ts +7 -3
  144. package/src/types/working-state.ts +2 -9
  145. package/src/views/settings/section-runtime-loop.tsx +0 -14
  146. package/src/app/api/canvas/[sessionId]/route.ts +0 -35
  147. package/src/app/api/missions/[id]/actions/route.ts +0 -31
  148. package/src/app/api/missions/[id]/events/route.ts +0 -14
  149. package/src/app/api/missions/[id]/route.ts +0 -10
  150. package/src/app/api/missions/route.test.ts +0 -244
  151. package/src/app/api/missions/route.ts +0 -57
  152. package/src/app/api/wallets/[id]/approve/route.ts +0 -79
  153. package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
  154. package/src/app/api/wallets/[id]/send/route.ts +0 -113
  155. package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
  156. package/src/app/missions/[id]/page.tsx +0 -3
  157. package/src/app/missions/page.tsx +0 -685
  158. package/src/components/canvas/canvas-panel.tsx +0 -267
  159. package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
  160. package/src/components/wallets/wallet-panel.tsx +0 -1010
  161. package/src/components/wallets/wallet-section.tsx +0 -260
  162. package/src/features/missions/queries.ts +0 -23
  163. package/src/lib/canvas-content.test.ts +0 -360
  164. package/src/lib/canvas-content.ts +0 -198
  165. package/src/lib/server/canvas-content.test.ts +0 -32
  166. package/src/lib/server/canvas-content.ts +0 -6
  167. package/src/lib/server/ethereum.ts +0 -591
  168. package/src/lib/server/evm-swap.ts +0 -476
  169. package/src/lib/server/missions/mission-intent.test.ts +0 -63
  170. package/src/lib/server/missions/mission-intent.ts +0 -569
  171. package/src/lib/server/missions/mission-repository.ts +0 -74
  172. package/src/lib/server/missions/mission-service/actions.ts +0 -6
  173. package/src/lib/server/missions/mission-service/bindings.ts +0 -9
  174. package/src/lib/server/missions/mission-service/context.ts +0 -4
  175. package/src/lib/server/missions/mission-service/core.ts +0 -2271
  176. package/src/lib/server/missions/mission-service/queries.ts +0 -12
  177. package/src/lib/server/missions/mission-service/recovery.ts +0 -5
  178. package/src/lib/server/missions/mission-service/ticks.ts +0 -9
  179. package/src/lib/server/missions/mission-service.test.ts +0 -888
  180. package/src/lib/server/missions/mission-service.ts +0 -6
  181. package/src/lib/server/session-tools/canvas.ts +0 -105
  182. package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
  183. package/src/lib/server/session-tools/wallet.ts +0 -1287
  184. package/src/lib/server/solana.ts +0 -327
  185. package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
  186. package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
  187. package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
  188. package/src/lib/server/wallet/wallet-service.test.ts +0 -81
  189. package/src/lib/server/wallet/wallet-service.ts +0 -225
  190. package/src/lib/wallet/wallet-transactions.test.ts +0 -75
  191. package/src/lib/wallet/wallet-transactions.ts +0 -43
  192. package/src/lib/wallet/wallet.test.ts +0 -333
  193. package/src/lib/wallet/wallet.ts +0 -183
  194. package/src/types/mission.ts +0 -185
  195. 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
- })