@swarmclawai/swarmclaw 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -15
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +7 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +1 -1
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +189 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +15 -10
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.ts +4 -2
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +210 -14
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool loop detection — modelled after OpenClaw's approach.
|
|
3
|
+
*
|
|
4
|
+
* Four detectors run on every on_tool_end event:
|
|
5
|
+
* 1. Generic repeat — same (name, inputHash) seen N+ times
|
|
6
|
+
* 2. Polling stall — repeated poll-like calls with identical output
|
|
7
|
+
* 3. Ping-pong — two tools alternating with identical results
|
|
8
|
+
* 4. Circuit breaker — absolute cap on identical calls regardless of type
|
|
9
|
+
*
|
|
10
|
+
* Each detector returns a severity: 'ok' | 'warning' | 'critical'.
|
|
11
|
+
* The caller decides what to do (log, inject guidance, abort).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from 'crypto'
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface ToolCallRecord {
|
|
21
|
+
name: string
|
|
22
|
+
inputHash: string
|
|
23
|
+
outputHash: string
|
|
24
|
+
/** first 200 chars of output for logging */
|
|
25
|
+
outputPreview: string
|
|
26
|
+
timestamp: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type LoopSeverity = 'ok' | 'warning' | 'critical'
|
|
30
|
+
|
|
31
|
+
export interface LoopDetectionResult {
|
|
32
|
+
severity: LoopSeverity
|
|
33
|
+
detector: 'generic_repeat' | 'polling_stall' | 'ping_pong' | 'circuit_breaker' | 'tool_frequency'
|
|
34
|
+
message: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LoopDetectionThresholds {
|
|
38
|
+
/** Generic repeat: warn after this many identical (name, input) calls. Default 6. */
|
|
39
|
+
repeatWarn: number
|
|
40
|
+
/** Generic repeat: critical after this many. Default 12. */
|
|
41
|
+
repeatCritical: number
|
|
42
|
+
/** Polling stall: warn after N poll-like calls with identical output. Default 4. */
|
|
43
|
+
pollWarn: number
|
|
44
|
+
/** Polling stall: critical after this many. Default 8. */
|
|
45
|
+
pollCritical: number
|
|
46
|
+
/** Ping-pong: how many alternating-pair cycles trigger warning. Default 3. */
|
|
47
|
+
pingPongWarn: number
|
|
48
|
+
/** Ping-pong: critical after this many cycles. Default 5. */
|
|
49
|
+
pingPongCritical: number
|
|
50
|
+
/** Circuit breaker: absolute cap on any identical call. Default 20. */
|
|
51
|
+
circuitBreaker: number
|
|
52
|
+
/** Per-tool frequency: warn after this many calls to the same tool (any input). Default 5. */
|
|
53
|
+
toolFrequencyWarn: number
|
|
54
|
+
/** Per-tool frequency: critical after this many calls to the same tool (any input). Default 8. */
|
|
55
|
+
toolFrequencyCritical: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DEFAULT_THRESHOLDS: LoopDetectionThresholds = {
|
|
59
|
+
repeatWarn: 6,
|
|
60
|
+
repeatCritical: 12,
|
|
61
|
+
pollWarn: 4,
|
|
62
|
+
pollCritical: 8,
|
|
63
|
+
pingPongWarn: 3,
|
|
64
|
+
pingPongCritical: 5,
|
|
65
|
+
circuitBreaker: 20,
|
|
66
|
+
toolFrequencyWarn: 3,
|
|
67
|
+
toolFrequencyCritical: 5,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Hash helpers
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
function quickHash(input: string): string {
|
|
75
|
+
return createHash('sha256').update(input).digest('hex').slice(0, 16)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function hashToolInput(input: unknown): string {
|
|
79
|
+
const str = typeof input === 'string' ? input : JSON.stringify(input ?? '')
|
|
80
|
+
return quickHash(str)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function hashToolOutput(output: unknown): string {
|
|
84
|
+
const str = typeof output === 'string' ? output : JSON.stringify(output ?? '')
|
|
85
|
+
return quickHash(str)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Tracker
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
export class ToolLoopTracker {
|
|
93
|
+
private history: ToolCallRecord[] = []
|
|
94
|
+
private thresholds: LoopDetectionThresholds
|
|
95
|
+
|
|
96
|
+
constructor(thresholds?: Partial<LoopDetectionThresholds>) {
|
|
97
|
+
this.thresholds = { ...DEFAULT_THRESHOLDS, ...thresholds }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Record a completed tool call and run all detectors. */
|
|
101
|
+
record(name: string, input: unknown, output: unknown): LoopDetectionResult | null {
|
|
102
|
+
const inputHash = hashToolInput(input)
|
|
103
|
+
const outputStr = typeof output === 'string' ? output : JSON.stringify(output ?? '')
|
|
104
|
+
const outputHash = hashToolOutput(output)
|
|
105
|
+
const record: ToolCallRecord = {
|
|
106
|
+
name,
|
|
107
|
+
inputHash,
|
|
108
|
+
outputHash,
|
|
109
|
+
outputPreview: outputStr.slice(0, 200),
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
}
|
|
112
|
+
this.history.push(record)
|
|
113
|
+
|
|
114
|
+
// Run detectors in severity order (most severe first)
|
|
115
|
+
return this.checkCircuitBreaker(record)
|
|
116
|
+
?? this.checkToolFrequency(record)
|
|
117
|
+
?? this.checkGenericRepeat(record)
|
|
118
|
+
?? this.checkPollingStall(record)
|
|
119
|
+
?? this.checkPingPong()
|
|
120
|
+
?? null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Get the full call history (for diagnostics). */
|
|
124
|
+
getHistory(): ReadonlyArray<ToolCallRecord> {
|
|
125
|
+
return this.history
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Total recorded calls. */
|
|
129
|
+
get size(): number {
|
|
130
|
+
return this.history.length
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// -------------------------------------------------------------------------
|
|
134
|
+
// Detectors
|
|
135
|
+
// -------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
private checkToolFrequency(current: ToolCallRecord): LoopDetectionResult | null {
|
|
138
|
+
let count = 0
|
|
139
|
+
for (const r of this.history) {
|
|
140
|
+
if (r.name === current.name) count++
|
|
141
|
+
}
|
|
142
|
+
if (count >= this.thresholds.toolFrequencyCritical) {
|
|
143
|
+
return {
|
|
144
|
+
severity: 'critical',
|
|
145
|
+
detector: 'tool_frequency',
|
|
146
|
+
message: `Tool "${current.name}" called ${count} times this turn. Excessive repetition — wrap up with available results.`,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (count >= this.thresholds.toolFrequencyWarn) {
|
|
150
|
+
return {
|
|
151
|
+
severity: 'warning',
|
|
152
|
+
detector: 'tool_frequency',
|
|
153
|
+
message: `Tool "${current.name}" called ${count} times. Consider whether more calls are needed.`,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private checkCircuitBreaker(current: ToolCallRecord): LoopDetectionResult | null {
|
|
160
|
+
const key = `${current.name}:${current.inputHash}`
|
|
161
|
+
let count = 0
|
|
162
|
+
for (const r of this.history) {
|
|
163
|
+
if (`${r.name}:${r.inputHash}` === key) count++
|
|
164
|
+
}
|
|
165
|
+
if (count >= this.thresholds.circuitBreaker) {
|
|
166
|
+
return {
|
|
167
|
+
severity: 'critical',
|
|
168
|
+
detector: 'circuit_breaker',
|
|
169
|
+
message: `Circuit breaker: "${current.name}" called ${count} times with identical input. Halting to prevent runaway.`,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private checkGenericRepeat(current: ToolCallRecord): LoopDetectionResult | null {
|
|
176
|
+
const key = `${current.name}:${current.inputHash}`
|
|
177
|
+
let count = 0
|
|
178
|
+
for (const r of this.history) {
|
|
179
|
+
if (`${r.name}:${r.inputHash}` === key) count++
|
|
180
|
+
}
|
|
181
|
+
if (count >= this.thresholds.repeatCritical) {
|
|
182
|
+
return {
|
|
183
|
+
severity: 'critical',
|
|
184
|
+
detector: 'generic_repeat',
|
|
185
|
+
message: `Tool "${current.name}" has been called ${count} times with the same input. This appears to be a stuck loop.`,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (count >= this.thresholds.repeatWarn) {
|
|
189
|
+
return {
|
|
190
|
+
severity: 'warning',
|
|
191
|
+
detector: 'generic_repeat',
|
|
192
|
+
message: `Tool "${current.name}" has been called ${count} times with the same input. Consider a different approach.`,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private checkPollingStall(current: ToolCallRecord): LoopDetectionResult | null {
|
|
199
|
+
// Look for recent sequential calls to the same tool with identical output
|
|
200
|
+
const recent = this.history.slice(-this.thresholds.pollCritical)
|
|
201
|
+
const pollRuns = recent.filter(
|
|
202
|
+
(r) => r.name === current.name && r.outputHash === current.outputHash,
|
|
203
|
+
)
|
|
204
|
+
if (pollRuns.length >= this.thresholds.pollCritical) {
|
|
205
|
+
return {
|
|
206
|
+
severity: 'critical',
|
|
207
|
+
detector: 'polling_stall',
|
|
208
|
+
message: `Polling stall: "${current.name}" returned identical output ${pollRuns.length} times consecutively. The polled resource is not changing.`,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (pollRuns.length >= this.thresholds.pollWarn) {
|
|
212
|
+
return {
|
|
213
|
+
severity: 'warning',
|
|
214
|
+
detector: 'polling_stall',
|
|
215
|
+
message: `Polling stall: "${current.name}" returned identical output ${pollRuns.length} times. The state may not be progressing.`,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private checkPingPong(): LoopDetectionResult | null {
|
|
222
|
+
const len = this.history.length
|
|
223
|
+
if (len < 4) return null
|
|
224
|
+
|
|
225
|
+
// Check if the last N calls form an A-B-A-B pattern with identical results
|
|
226
|
+
const last = this.history[len - 1]
|
|
227
|
+
const prev = this.history[len - 2]
|
|
228
|
+
if (last.name === prev.name) return null // same tool — not ping-pong
|
|
229
|
+
|
|
230
|
+
let cycles = 0
|
|
231
|
+
for (let i = len - 2; i >= 1; i -= 2) {
|
|
232
|
+
const a = this.history[i]
|
|
233
|
+
const b = this.history[i - 1]
|
|
234
|
+
if (
|
|
235
|
+
a.name === last.name && a.outputHash === last.outputHash
|
|
236
|
+
&& b.name === prev.name && b.outputHash === prev.outputHash
|
|
237
|
+
) {
|
|
238
|
+
cycles++
|
|
239
|
+
} else {
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (cycles >= this.thresholds.pingPongCritical) {
|
|
245
|
+
return {
|
|
246
|
+
severity: 'critical',
|
|
247
|
+
detector: 'ping_pong',
|
|
248
|
+
message: `Ping-pong: "${prev.name}" and "${last.name}" are alternating with identical results (${cycles} cycles). Breaking the loop.`,
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (cycles >= this.thresholds.pingPongWarn) {
|
|
252
|
+
return {
|
|
253
|
+
severity: 'warning',
|
|
254
|
+
detector: 'ping_pong',
|
|
255
|
+
message: `Ping-pong: "${prev.name}" and "${last.name}" may be stuck in an alternating loop (${cycles} cycles).`,
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -11,6 +11,8 @@ export const TOOL_CAPABILITY = {
|
|
|
11
11
|
deliveryMessage: 'delivery.message',
|
|
12
12
|
deliveryMedia: 'delivery.media',
|
|
13
13
|
deliveryVoiceNote: 'delivery.voice_note',
|
|
14
|
+
walletInspect: 'wallet.inspect',
|
|
15
|
+
walletExecute: 'wallet.execute',
|
|
14
16
|
} as const
|
|
15
17
|
|
|
16
18
|
export interface ToolPlanningEntry {
|
|
@@ -59,7 +61,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
59
61
|
requestMatchers: [
|
|
60
62
|
{
|
|
61
63
|
capability: TOOL_CAPABILITY.researchSearch,
|
|
62
|
-
patterns: ['research', 'look up', 'find out', 'search for', 'compare', 'latest', 'news', 'headline', 'current event', 'recent update', "what's new", 'what happened'],
|
|
64
|
+
patterns: ['research', 'look up', 'find out', 'search for', 'compare', 'latest', 'news', 'headline', 'current event', 'recent update', 'update', 'updates', 'breaking', 'developments', 'keep watching', 'watch for', 'watching for', 'monitor', 'track', "what's new", 'what happened'],
|
|
63
65
|
forbidLiteralUrl: true,
|
|
64
66
|
},
|
|
65
67
|
],
|
|
@@ -121,7 +123,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
|
|
|
121
123
|
},
|
|
122
124
|
{
|
|
123
125
|
capability: TOOL_CAPABILITY.deliveryMedia,
|
|
124
|
-
patterns: ['screenshot', 'screen shot', 'snapshot', 'image', 'photo', 'file', 'pdf', 'attachment'],
|
|
126
|
+
patterns: ['screenshot', 'screen shot', 'snapshot', 'image', 'photo', 'send file', 'send a file', 'pdf', 'attachment'],
|
|
125
127
|
},
|
|
126
128
|
{
|
|
127
129
|
capability: TOOL_CAPABILITY.deliveryVoiceNote,
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { after, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
import type { AgentWallet } from '@/types'
|
|
8
|
+
|
|
9
|
+
const originalEnv = {
|
|
10
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
11
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
12
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
13
|
+
CREDENTIAL_SECRET: process.env.CREDENTIAL_SECRET,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let tempDir = ''
|
|
17
|
+
let encryptKey: typeof import('./storage').encryptKey
|
|
18
|
+
let callEthereumContract: typeof import('./ethereum').callEthereumContract
|
|
19
|
+
let encodeEthereumContractCall: typeof import('./ethereum').encodeEthereumContractCall
|
|
20
|
+
let prepareEvmSwapPlan: typeof import('./evm-swap').prepareEvmSwapPlan
|
|
21
|
+
let signEthereumMessage: typeof import('./ethereum').signEthereumMessage
|
|
22
|
+
let signEthereumTypedData: typeof import('./ethereum').signEthereumTypedData
|
|
23
|
+
let generateSolanaKeypair: typeof import('./solana').generateSolanaKeypair
|
|
24
|
+
let signSolanaMessage: typeof import('./solana').signSolanaMessage
|
|
25
|
+
let signSolanaTransaction: typeof import('./solana').signSolanaTransaction
|
|
26
|
+
let TransactionCtor: typeof import('@solana/web3.js').Transaction
|
|
27
|
+
let SystemProgramNs: typeof import('@solana/web3.js').SystemProgram
|
|
28
|
+
let PublicKeyCtor: typeof import('@solana/web3.js').PublicKey
|
|
29
|
+
|
|
30
|
+
before(async () => {
|
|
31
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-wallet-exec-'))
|
|
32
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
33
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
34
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
35
|
+
process.env.CREDENTIAL_SECRET = '11'.repeat(32)
|
|
36
|
+
fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
|
|
37
|
+
fs.mkdirSync(process.env.WORKSPACE_DIR, { recursive: true })
|
|
38
|
+
|
|
39
|
+
;({ encryptKey } = await import('./storage'))
|
|
40
|
+
;({ callEthereumContract, encodeEthereumContractCall, signEthereumMessage, signEthereumTypedData } = await import('./ethereum'))
|
|
41
|
+
;({ prepareEvmSwapPlan } = await import('./evm-swap'))
|
|
42
|
+
;({ generateSolanaKeypair, signSolanaMessage, signSolanaTransaction } = await import('./solana'))
|
|
43
|
+
;({ Transaction: TransactionCtor, SystemProgram: SystemProgramNs, PublicKey: PublicKeyCtor } = await import('@solana/web3.js'))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
after(() => {
|
|
47
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
48
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
49
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
50
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
51
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
52
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
53
|
+
if (originalEnv.CREDENTIAL_SECRET === undefined) delete process.env.CREDENTIAL_SECRET
|
|
54
|
+
else process.env.CREDENTIAL_SECRET = originalEnv.CREDENTIAL_SECRET
|
|
55
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('wallet execution helpers', () => {
|
|
59
|
+
it('encodes ERC-20 contract calldata and signs EVM payloads', async () => {
|
|
60
|
+
const privateKey = '0x59c6995e998f97a5a004497e5d4ab3d89165b0def05d6d33923995df83329538'
|
|
61
|
+
const encrypted = encryptKey(privateKey)
|
|
62
|
+
|
|
63
|
+
const encoded = encodeEthereumContractCall(
|
|
64
|
+
['function approve(address spender,uint256 amount)'],
|
|
65
|
+
'approve',
|
|
66
|
+
['0x000000000000000000000000000000000000dEaD', '1000'],
|
|
67
|
+
)
|
|
68
|
+
assert.equal(encoded.data.startsWith('0x095ea7b3'), true)
|
|
69
|
+
|
|
70
|
+
const encodedFromNamedArgs = encodeEthereumContractCall(
|
|
71
|
+
['function approve(address spender,uint256 amount)'],
|
|
72
|
+
'approve',
|
|
73
|
+
{ spender: '0x000000000000000000000000000000000000dEaD', amount: '1000' },
|
|
74
|
+
)
|
|
75
|
+
assert.equal(encodedFromNamedArgs.data, encoded.data)
|
|
76
|
+
|
|
77
|
+
const encodedTupleArg = encodeEthereumContractCall(
|
|
78
|
+
['function quoteExactInputSingle((address tokenIn,address tokenOut,uint256 amountIn,uint24 fee,uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut)'],
|
|
79
|
+
'quoteExactInputSingle',
|
|
80
|
+
{
|
|
81
|
+
tokenIn: '0x0000000000000000000000000000000000000001',
|
|
82
|
+
tokenOut: '0x0000000000000000000000000000000000000002',
|
|
83
|
+
amountIn: '1000000',
|
|
84
|
+
fee: 500,
|
|
85
|
+
sqrtPriceLimitX96: '0',
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
assert.equal(encodedTupleArg.data.startsWith('0xc6a5026a'), true)
|
|
89
|
+
|
|
90
|
+
const signedMessage = await signEthereumMessage(encrypted, { message: 'hello world' })
|
|
91
|
+
assert.equal(signedMessage.address.length, 42)
|
|
92
|
+
assert.equal(signedMessage.signature.startsWith('0x'), true)
|
|
93
|
+
|
|
94
|
+
const signedTypedData = await signEthereumTypedData(encrypted, {
|
|
95
|
+
domain: {
|
|
96
|
+
name: 'SwarmClaw',
|
|
97
|
+
version: '1',
|
|
98
|
+
chainId: 1,
|
|
99
|
+
},
|
|
100
|
+
types: {
|
|
101
|
+
Login: [
|
|
102
|
+
{ name: 'wallet', type: 'address' },
|
|
103
|
+
{ name: 'nonce', type: 'uint256' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
value: {
|
|
107
|
+
wallet: signedMessage.address,
|
|
108
|
+
nonce: '7',
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
assert.equal(signedTypedData.signature.startsWith('0x'), true)
|
|
112
|
+
|
|
113
|
+
const called = await callEthereumContract(encrypted, {
|
|
114
|
+
contractAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
|
|
115
|
+
abi: ['function name() view returns (string)'],
|
|
116
|
+
functionName: 'name',
|
|
117
|
+
}, {
|
|
118
|
+
network: 'ethereum',
|
|
119
|
+
rpcUrl: 'https://ethereum-rpc.publicnode.com',
|
|
120
|
+
})
|
|
121
|
+
assert.equal(called.decoded, 'Wrapped Ether')
|
|
122
|
+
|
|
123
|
+
const allowance = await callEthereumContract(encrypted, {
|
|
124
|
+
contractAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
|
|
125
|
+
abi: ['function allowance(address owner,address spender) view returns (uint256)'],
|
|
126
|
+
functionName: 'allowance',
|
|
127
|
+
args: {
|
|
128
|
+
owner: signedMessage.address,
|
|
129
|
+
spender: '0x000000000000000000000000000000000000dEaD',
|
|
130
|
+
},
|
|
131
|
+
}, {
|
|
132
|
+
network: 'ethereum',
|
|
133
|
+
rpcUrl: 'https://ethereum-rpc.publicnode.com',
|
|
134
|
+
})
|
|
135
|
+
assert.equal(typeof allowance.decoded, 'string')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('builds a generic ParaSwap-backed swap plan for Arbitrum without a venue-specific adapter', async () => {
|
|
139
|
+
const privateKey = '0x59c6995e998f97a5a004497e5d4ab3d89165b0def05d6d33923995df83329538'
|
|
140
|
+
const encrypted = encryptKey(privateKey)
|
|
141
|
+
const walletAddress = (await signEthereumMessage(encrypted, { message: 'derive address' })).address
|
|
142
|
+
const wallet: AgentWallet = {
|
|
143
|
+
id: 'wallet_swap_plan',
|
|
144
|
+
agentId: 'agent_wallet',
|
|
145
|
+
chain: 'ethereum',
|
|
146
|
+
publicKey: walletAddress,
|
|
147
|
+
encryptedPrivateKey: encrypted,
|
|
148
|
+
spendingLimitAtomic: '1000000000000000000',
|
|
149
|
+
dailyLimitAtomic: '10000000000000000000',
|
|
150
|
+
requireApproval: true,
|
|
151
|
+
createdAt: Date.now(),
|
|
152
|
+
updatedAt: Date.now(),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const plan = await prepareEvmSwapPlan({
|
|
156
|
+
wallet,
|
|
157
|
+
network: 'arbitrum',
|
|
158
|
+
sellToken: 'USDC',
|
|
159
|
+
buyToken: 'ETH',
|
|
160
|
+
sellAmountDisplay: '1',
|
|
161
|
+
skipBalanceCheck: true,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
assert.equal(plan.provider, 'paraswap')
|
|
165
|
+
assert.equal(plan.network.id, 'arbitrum')
|
|
166
|
+
assert.equal(plan.sellToken.symbol, 'USDC')
|
|
167
|
+
assert.equal(plan.buyToken.symbol, 'ETH')
|
|
168
|
+
assert.equal(plan.sellAmountAtomic, '1000000')
|
|
169
|
+
assert.equal(plan.approvalRequired, true)
|
|
170
|
+
assert.equal(typeof plan.spenderAddress, 'string')
|
|
171
|
+
assert.equal(typeof plan.swapTransaction.to, 'string')
|
|
172
|
+
assert.equal(String(plan.swapTransaction.data || '').startsWith('0x'), true)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('signs Solana messages and legacy transactions offline', async () => {
|
|
176
|
+
const sender = generateSolanaKeypair()
|
|
177
|
+
const recipient = generateSolanaKeypair()
|
|
178
|
+
|
|
179
|
+
const signedMessage = await signSolanaMessage(sender.encryptedPrivateKey, { message: 'solana hello' })
|
|
180
|
+
assert.equal(signedMessage.publicKey, sender.publicKey)
|
|
181
|
+
assert.equal(signedMessage.signature.length > 40, true)
|
|
182
|
+
|
|
183
|
+
const tx = new TransactionCtor()
|
|
184
|
+
tx.feePayer = new PublicKeyCtor(sender.publicKey)
|
|
185
|
+
tx.recentBlockhash = generateSolanaKeypair().publicKey
|
|
186
|
+
tx.add(SystemProgramNs.transfer({
|
|
187
|
+
fromPubkey: new PublicKeyCtor(sender.publicKey),
|
|
188
|
+
toPubkey: new PublicKeyCtor(recipient.publicKey),
|
|
189
|
+
lamports: 1_234,
|
|
190
|
+
}))
|
|
191
|
+
|
|
192
|
+
const unsignedBase64 = Buffer.from(tx.serialize({ requireAllSignatures: false, verifySignatures: false })).toString('base64')
|
|
193
|
+
const signedTx = await signSolanaTransaction(sender.encryptedPrivateKey, unsignedBase64)
|
|
194
|
+
assert.equal(signedTx.publicKey, sender.publicKey)
|
|
195
|
+
assert.equal(signedTx.signatures.length > 0, true)
|
|
196
|
+
assert.equal(typeof signedTx.signedTransactionBase64, 'string')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import type { AgentWallet } from '@/types'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildLogDiscoveryRanges,
|
|
7
|
+
estimateDiscoveryStartBlock,
|
|
8
|
+
getKnownEvmTokenContracts,
|
|
9
|
+
parseMetaplexMetadataFields,
|
|
10
|
+
buildEmptyWalletPortfolio,
|
|
11
|
+
resolveWalletPortfolioWithTimeout,
|
|
12
|
+
} from './wallet-portfolio'
|
|
13
|
+
|
|
14
|
+
describe('wallet portfolio helpers', () => {
|
|
15
|
+
it('splits large log discovery requests into provider-safe chunks', () => {
|
|
16
|
+
assert.deepEqual(buildLogDiscoveryRanges(10, 10, 50_000), [{ fromBlock: 10, toBlock: 10 }])
|
|
17
|
+
assert.deepEqual(buildLogDiscoveryRanges(1, 120_000, 50_000), [
|
|
18
|
+
{ fromBlock: 1, toBlock: 50_000 },
|
|
19
|
+
{ fromBlock: 50_001, toBlock: 100_000 },
|
|
20
|
+
{ fromBlock: 100_001, toBlock: 120_000 },
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('always checks canonical USDC contracts on supported EVM networks', () => {
|
|
25
|
+
assert.equal(
|
|
26
|
+
getKnownEvmTokenContracts('arbitrum').map((address) => address.toLowerCase()).includes('0xaf88d065e77c8cc2239327c5edb3a432268e5831'),
|
|
27
|
+
true,
|
|
28
|
+
)
|
|
29
|
+
assert.equal(
|
|
30
|
+
getKnownEvmTokenContracts('base').map((address) => address.toLowerCase()).includes('0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'),
|
|
31
|
+
true,
|
|
32
|
+
)
|
|
33
|
+
assert.equal(
|
|
34
|
+
getKnownEvmTokenContracts('ethereum').map((address) => address.toLowerCase()).includes('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'),
|
|
35
|
+
true,
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('scans from wallet age rather than capping discovery to a fixed recent window', () => {
|
|
40
|
+
const now = Date.UTC(2026, 2, 8)
|
|
41
|
+
const latestBlock = 10_000_000
|
|
42
|
+
const newerWalletStart = estimateDiscoveryStartBlock({
|
|
43
|
+
latestBlock,
|
|
44
|
+
walletCreatedAt: now - (7 * 24 * 60 * 60 * 1000),
|
|
45
|
+
avgBlockMs: 12_000,
|
|
46
|
+
maxDiscoveryBlocks: 5_000_000,
|
|
47
|
+
now,
|
|
48
|
+
})
|
|
49
|
+
const olderWalletStart = estimateDiscoveryStartBlock({
|
|
50
|
+
latestBlock,
|
|
51
|
+
walletCreatedAt: now - (30 * 24 * 60 * 60 * 1000),
|
|
52
|
+
avgBlockMs: 12_000,
|
|
53
|
+
maxDiscoveryBlocks: 5_000_000,
|
|
54
|
+
now,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
assert.equal(olderWalletStart < newerWalletStart, true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('parses metaplex metadata name and symbol for arbitrary SPL mints', () => {
|
|
61
|
+
const data = Buffer.alloc(1 + 32 + 32 + 32 + 10)
|
|
62
|
+
Buffer.from('Example Token').copy(data, 1 + 32 + 32)
|
|
63
|
+
Buffer.from('EXMPL').copy(data, 1 + 32 + 32 + 32)
|
|
64
|
+
|
|
65
|
+
assert.deepEqual(parseMetaplexMetadataFields(data), {
|
|
66
|
+
name: 'Example Token',
|
|
67
|
+
symbol: 'EXMPL',
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns stale portfolio data when a live portfolio lookup times out', async () => {
|
|
72
|
+
const wallet: AgentWallet = {
|
|
73
|
+
id: 'wallet-timeout',
|
|
74
|
+
agentId: 'agent-timeout',
|
|
75
|
+
chain: 'ethereum',
|
|
76
|
+
publicKey: '0x0000000000000000000000000000000000000001',
|
|
77
|
+
encryptedPrivateKey: 'secret',
|
|
78
|
+
requireApproval: true,
|
|
79
|
+
spendingLimitAtomic: '1',
|
|
80
|
+
dailyLimitAtomic: '1',
|
|
81
|
+
createdAt: 1,
|
|
82
|
+
updatedAt: 1,
|
|
83
|
+
}
|
|
84
|
+
const stale = buildEmptyWalletPortfolio(wallet)
|
|
85
|
+
stale.balanceAtomic = '123'
|
|
86
|
+
stale.balanceFormatted = '0.000000000000000123'
|
|
87
|
+
stale.balanceDisplay = `${stale.balanceFormatted} ETH`
|
|
88
|
+
|
|
89
|
+
const result = await resolveWalletPortfolioWithTimeout({
|
|
90
|
+
load: () => new Promise<ReturnType<typeof buildEmptyWalletPortfolio>>(() => {}),
|
|
91
|
+
timeoutMs: 5,
|
|
92
|
+
stale,
|
|
93
|
+
label: 'wallet portfolio timeout test',
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
assert.equal(result.balanceAtomic, '123')
|
|
97
|
+
})
|
|
98
|
+
})
|