@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.
- package/README.md +39 -6
- package/package.json +2 -2
- package/src/app/agents/[id]/page.tsx +1 -18
- package/src/app/api/activity/route.ts +9 -23
- package/src/app/api/agents/route.ts +17 -1
- package/src/app/api/agents/thread-route.test.ts +0 -1
- package/src/app/api/approvals/route.test.ts +6 -22
- package/src/app/api/approvals/route.ts +13 -5
- package/src/app/api/connectors/route.ts +2 -2
- package/src/app/api/credentials/[id]/route.ts +2 -0
- package/src/app/api/credentials/route.ts +4 -1
- package/src/app/api/goals/[id]/route.ts +28 -0
- package/src/app/api/goals/route.ts +33 -0
- package/src/app/api/portability/export/route.ts +8 -0
- package/src/app/api/portability/import/route.test.ts +80 -0
- package/src/app/api/portability/import/route.ts +28 -0
- package/src/app/api/protocols/templates/[id]/route.ts +2 -1
- package/src/app/api/protocols/templates/route.ts +2 -1
- package/src/app/api/settings/route.ts +13 -2
- package/src/app/api/wallets/[id]/route.ts +15 -157
- package/src/app/api/wallets/generate/route.ts +22 -0
- package/src/app/api/wallets/route.test.ts +147 -0
- package/src/app/api/wallets/route.ts +13 -95
- package/src/app/autonomy/page.tsx +2 -57
- package/src/app/home/page.tsx +3 -0
- package/src/app/protocols/page.tsx +2 -21
- package/src/app/settings/page.tsx +0 -9
- package/src/app/wallets/page.tsx +105 -5
- package/src/cli/index.js +32 -33
- package/src/cli/spec.js +26 -27
- package/src/components/agents/agent-sheet.tsx +2 -40
- package/src/components/agents/inspector-panel.tsx +0 -83
- package/src/components/chat/chat-card.tsx +0 -31
- package/src/components/chat/message-bubble.tsx +1 -108
- package/src/components/connectors/connector-sheet.tsx +25 -1
- package/src/components/layout/sidebar-rail.tsx +6 -10
- package/src/components/projects/project-detail.tsx +3 -35
- package/src/components/projects/tabs/overview-tab.tsx +3 -59
- package/src/components/projects/tabs/work-tab.tsx +7 -77
- package/src/components/protocols/structured-session-launcher.tsx +1 -22
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/tasks/task-card.tsx +4 -34
- package/src/components/tasks/task-sheet.tsx +6 -36
- package/src/components/wallets/wallet-list.tsx +150 -0
- package/src/lib/app/navigation.test.ts +0 -13
- package/src/lib/app/navigation.ts +2 -7
- package/src/lib/app/view-constants.ts +14 -19
- package/src/lib/server/activity/activity-log.ts +16 -1
- package/src/lib/server/agents/agent-service.ts +24 -11
- package/src/lib/server/agents/agent-thread-session.ts +0 -1
- package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
- package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
- package/src/lib/server/agents/delegation-jobs.ts +0 -25
- package/src/lib/server/agents/main-agent-loop.ts +1 -49
- package/src/lib/server/agents/subagent-runtime.ts +0 -1
- package/src/lib/server/approval-match.ts +14 -85
- package/src/lib/server/approvals/approval-hooks.ts +81 -0
- package/src/lib/server/approvals.test.ts +6 -6
- package/src/lib/server/approvals.ts +11 -6
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
- package/src/lib/server/builtin-extensions.ts +0 -2
- package/src/lib/server/capability-router.test.ts +0 -2
- package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +14 -14
- package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
- package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -2
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +2 -22
- package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
- package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
- package/src/lib/server/chat-execution/message-classifier.ts +1 -16
- package/src/lib/server/chat-execution/prompt-builder.test.ts +0 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +0 -30
- package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
- package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
- package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +8 -123
- package/src/lib/server/chat-execution/stream-agent-chat.ts +1 -5
- package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
- package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
- package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
- package/src/lib/server/chats/chat-session-service.ts +3 -5
- package/src/lib/server/connectors/connector-inbound.ts +0 -1
- package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
- package/src/lib/server/connectors/connector-service.ts +39 -9
- package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
- package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
- package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
- package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
- package/src/lib/server/connectors/swarmdock-tasks.ts +127 -0
- package/src/lib/server/connectors/swarmdock.ts +285 -0
- package/src/lib/server/execution-brief.test.ts +2 -25
- package/src/lib/server/execution-brief.ts +30 -35
- package/src/lib/server/execution-engine/task-attempt.ts +0 -1
- package/src/lib/server/goals/goal-repository.ts +19 -0
- package/src/lib/server/goals/goal-service.ts +143 -0
- package/src/lib/server/persistence/storage-context.ts +0 -5
- package/src/lib/server/portability/export.ts +109 -0
- package/src/lib/server/portability/import.ts +159 -0
- package/src/lib/server/protocols/protocol-normalization.ts +0 -4
- package/src/lib/server/protocols/protocol-queries.ts +0 -6
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
- package/src/lib/server/protocols/protocol-service.ts +0 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
- package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
- package/src/lib/server/protocols/protocol-swarm.ts +0 -2
- package/src/lib/server/protocols/protocol-types.ts +0 -2
- package/src/lib/server/provider-health.ts +0 -9
- package/src/lib/server/runtime/daemon-state/core.ts +0 -9
- package/src/lib/server/runtime/daemon-state.test.ts +0 -35
- package/src/lib/server/runtime/heartbeat-service.ts +3 -23
- package/src/lib/server/runtime/queue/core.ts +11 -33
- package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
- package/src/lib/server/runtime/scheduler.ts +0 -13
- package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
- package/src/lib/server/runtime/session-run-manager/queries.ts +0 -1
- package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
- package/src/lib/server/runtime/session-run-manager.test.ts +0 -28
- package/src/lib/server/session-tools/crud.ts +0 -14
- package/src/lib/server/session-tools/delegate.ts +0 -4
- package/src/lib/server/session-tools/index.ts +0 -4
- package/src/lib/server/session-tools/team-context.ts +0 -3
- package/src/lib/server/storage-normalization.ts +13 -0
- package/src/lib/server/storage.ts +75 -45
- package/src/lib/server/tasks/task-checkout.ts +59 -0
- package/src/lib/server/tasks/task-lifecycle.ts +2 -0
- package/src/lib/server/tasks/task-route-service.ts +4 -26
- package/src/lib/server/tasks/task-service.ts +0 -7
- package/src/lib/server/tool-aliases.ts +0 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +4 -4
- package/src/lib/server/tool-capability-policy.ts +0 -2
- package/src/lib/server/tool-planning.ts +0 -12
- package/src/lib/server/universal-tool-access.ts +0 -1
- package/src/lib/server/usage/cost-rollup.ts +124 -0
- package/src/lib/server/usage/usage-repository.ts +6 -0
- package/src/lib/server/wallets/wallet-crypto.ts +33 -0
- package/src/lib/server/wallets/wallet-repository.ts +24 -0
- package/src/lib/server/wallets/wallet-service.ts +119 -0
- package/src/lib/server/working-state/extraction.ts +8 -42
- package/src/lib/server/working-state/normalization.ts +10 -103
- package/src/lib/server/working-state/service.ts +12 -21
- package/src/lib/strip-internal-metadata.test.ts +1 -1
- package/src/lib/strip-internal-metadata.ts +1 -1
- package/src/lib/tool-definitions.ts +0 -1
- package/src/lib/validation/schemas.ts +36 -32
- package/src/lib/validation/server-schemas.ts +35 -0
- package/src/stores/slices/data-slice.ts +5 -1
- package/src/stores/slices/ui-slice.ts +0 -4
- package/src/types/agent.ts +10 -84
- package/src/types/app-settings.ts +6 -2
- package/src/types/approval.ts +3 -2
- package/src/types/connector.ts +1 -0
- package/src/types/goal.ts +30 -0
- package/src/types/index.ts +2 -1
- package/src/types/message.ts +0 -1
- package/src/types/misc.ts +2 -4
- package/src/types/protocol.ts +0 -2
- package/src/types/run.ts +0 -3
- package/src/types/session.ts +1 -51
- package/src/types/swarmdock.ts +29 -0
- package/src/types/task.ts +9 -3
- package/src/types/working-state.ts +2 -9
- package/src/views/settings/section-runtime-loop.tsx +0 -14
- package/src/app/api/canvas/[sessionId]/route.ts +0 -35
- package/src/app/api/missions/[id]/actions/route.ts +0 -31
- package/src/app/api/missions/[id]/events/route.ts +0 -14
- package/src/app/api/missions/[id]/route.ts +0 -10
- package/src/app/api/missions/route.test.ts +0 -244
- package/src/app/api/missions/route.ts +0 -57
- package/src/app/api/wallets/[id]/approve/route.ts +0 -79
- package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
- package/src/app/api/wallets/[id]/send/route.ts +0 -113
- package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
- package/src/app/missions/[id]/page.tsx +0 -3
- package/src/app/missions/page.tsx +0 -685
- package/src/components/canvas/canvas-panel.tsx +0 -267
- package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
- package/src/components/wallets/wallet-panel.tsx +0 -1010
- package/src/components/wallets/wallet-section.tsx +0 -260
- package/src/features/missions/queries.ts +0 -23
- package/src/lib/canvas-content.test.ts +0 -360
- package/src/lib/canvas-content.ts +0 -198
- package/src/lib/server/canvas-content.test.ts +0 -32
- package/src/lib/server/canvas-content.ts +0 -6
- package/src/lib/server/ethereum.ts +0 -591
- package/src/lib/server/evm-swap.ts +0 -476
- package/src/lib/server/missions/mission-intent.test.ts +0 -63
- package/src/lib/server/missions/mission-intent.ts +0 -569
- package/src/lib/server/missions/mission-repository.ts +0 -74
- package/src/lib/server/missions/mission-service/actions.ts +0 -6
- package/src/lib/server/missions/mission-service/bindings.ts +0 -9
- package/src/lib/server/missions/mission-service/context.ts +0 -4
- package/src/lib/server/missions/mission-service/core.ts +0 -2271
- package/src/lib/server/missions/mission-service/queries.ts +0 -12
- package/src/lib/server/missions/mission-service/recovery.ts +0 -5
- package/src/lib/server/missions/mission-service/ticks.ts +0 -9
- package/src/lib/server/missions/mission-service.test.ts +0 -888
- package/src/lib/server/missions/mission-service.ts +0 -6
- package/src/lib/server/session-tools/canvas.ts +0 -105
- package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
- package/src/lib/server/session-tools/wallet.ts +0 -1287
- package/src/lib/server/solana.ts +0 -327
- package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
- package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
- package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
- package/src/lib/server/wallet/wallet-service.test.ts +0 -81
- package/src/lib/server/wallet/wallet-service.ts +0 -225
- package/src/lib/wallet/wallet-transactions.test.ts +0 -75
- package/src/lib/wallet/wallet-transactions.ts +0 -43
- package/src/lib/wallet/wallet.test.ts +0 -333
- package/src/lib/wallet/wallet.ts +0 -183
- package/src/types/mission.ts +0 -185
- 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
|
-
})
|