@agentlayer.tech/wallet 0.1.11 → 0.1.13
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/.openclaw/extensions/agent-wallet/index.ts +454 -18
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +96 -0
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +2 -0
- package/CHANGELOG.md +25 -0
- package/README.md +43 -51
- package/agent-wallet/.env.example +11 -0
- package/agent-wallet/README.md +53 -0
- package/agent-wallet/agent_wallet/approval.py +4 -0
- package/agent-wallet/agent_wallet/config.py +6 -0
- package/agent-wallet/agent_wallet/exceptions.py +2 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +361 -2
- package/agent-wallet/agent_wallet/openclaw_cli.py +13 -1
- package/agent-wallet/agent_wallet/openclaw_runtime.py +2 -5
- package/agent-wallet/agent_wallet/providers/houdini.py +539 -0
- package/agent-wallet/agent_wallet/transaction_policy.py +251 -0
- package/agent-wallet/agent_wallet/user_wallets.py +83 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +40 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +885 -16
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +291 -0
- package/agent-wallet/scripts/install_agent_wallet.py +54 -2
- package/agent-wallet/scripts/install_openclaw_local_config.py +84 -4
- package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +343 -0
- package/agent-wallet/scripts/setup_evm_wallet.sh +151 -0
- package/hermes/plugins/agent_wallet/__init__.py +28 -2
- package/hermes/plugins/agent_wallet/plugin.yaml +2 -0
- package/hermes/plugins/agent_wallet/schemas.py +72 -0
- package/hermes/plugins/agent_wallet/tools.py +193 -9
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { promisify } from "node:util";
|
|
@@ -10,6 +11,182 @@ let selectedWalletBackend = null;
|
|
|
10
11
|
let selectedSolanaNetwork = null;
|
|
11
12
|
let selectedEvmNetwork = null;
|
|
12
13
|
let selectedBtcNetwork = null;
|
|
14
|
+
const PREVIEW_CACHE_TTL_MS = 15 * 60 * 1000;
|
|
15
|
+
const PRIVATE_SWAP_CACHE_TTL_MS = 35 * 60 * 1000;
|
|
16
|
+
const PREVIEW_BOUND_SWAP_TOOLS = new Set(["swap_solana_tokens", "swap_solana_privately"]);
|
|
17
|
+
const PRIVATE_SWAP_APPROVAL_TOOL_NAME = "swap_solana_privately";
|
|
18
|
+
const approvalPreviewCache = new Map();
|
|
19
|
+
const privateSwapOrderCache = new Map();
|
|
20
|
+
const WALLET_TOOL_ONLY_GUIDANCE =
|
|
21
|
+
"Use this wallet tool instead of shelling out to solana CLI, spl-token CLI, curl, or exec. If it fails, surface the wallet-tool error and stop rather than falling back to terminal commands.";
|
|
22
|
+
|
|
23
|
+
function canonicalJsonText(payload) {
|
|
24
|
+
const normalize = (value) => {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return value.map(normalize);
|
|
27
|
+
}
|
|
28
|
+
if (value && typeof value === "object") {
|
|
29
|
+
return Object.fromEntries(
|
|
30
|
+
Object.keys(value)
|
|
31
|
+
.sort()
|
|
32
|
+
.map((key) => [key, normalize(value[key])])
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
return JSON.stringify(normalize(payload));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function previewDigest(preview) {
|
|
41
|
+
return crypto.createHash("sha256").update(canonicalJsonText(preview), "utf8").digest("hex");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function approvalCacheKey(userId, toolName) {
|
|
45
|
+
return `${userId}::${toolName}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pruneApprovalPreviewCache() {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
for (const [key, value] of approvalPreviewCache.entries()) {
|
|
51
|
+
if (!value || typeof value !== "object" || Number(value.expiresAt || 0) <= now) {
|
|
52
|
+
approvalPreviewCache.delete(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cachePreviewForApproval(userId, toolName, payload) {
|
|
58
|
+
if (!payload || payload.ok !== true || !payload.data || typeof payload.data !== "object") return;
|
|
59
|
+
const preview = payload.data;
|
|
60
|
+
if (preview.mode !== "preview") return;
|
|
61
|
+
if (!preview.confirmation_summary || typeof preview.confirmation_summary !== "object") return;
|
|
62
|
+
pruneApprovalPreviewCache();
|
|
63
|
+
const digest = previewDigest(preview);
|
|
64
|
+
approvalPreviewCache.set(approvalCacheKey(userId, toolName), {
|
|
65
|
+
digest,
|
|
66
|
+
expiresAt:
|
|
67
|
+
toolName === "swap_solana_privately"
|
|
68
|
+
? Date.now() + PRIVATE_SWAP_CACHE_TTL_MS
|
|
69
|
+
: Date.now() + PREVIEW_CACHE_TTL_MS,
|
|
70
|
+
preview,
|
|
71
|
+
summary: preview.confirmation_summary,
|
|
72
|
+
});
|
|
73
|
+
if (toolName === "swap_solana_privately") {
|
|
74
|
+
privateSwapOrderCache.delete(approvalCacheKey(userId, toolName));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function latestCachedPreview(userId, toolName) {
|
|
79
|
+
pruneApprovalPreviewCache();
|
|
80
|
+
return approvalPreviewCache.get(approvalCacheKey(userId, toolName)) || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function approvalTokenPreviewDigest(token) {
|
|
84
|
+
if (typeof token !== "string" || !token.includes(".")) return "";
|
|
85
|
+
try {
|
|
86
|
+
const encoded = token.split(".", 1)[0];
|
|
87
|
+
const payload = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
88
|
+
const summary = payload?.binding?.summary;
|
|
89
|
+
return summary && typeof summary._preview_digest === "string" ? summary._preview_digest : "";
|
|
90
|
+
} catch {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function cachedPreviewForToken(userId, toolName, token) {
|
|
96
|
+
const digest = approvalTokenPreviewDigest(token);
|
|
97
|
+
if (!digest) return null;
|
|
98
|
+
const cached = latestCachedPreview(userId, toolName);
|
|
99
|
+
if (!cached || cached.digest !== digest) return null;
|
|
100
|
+
return cached.preview && typeof cached.preview === "object" ? cached.preview : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function cachePendingPrivateSwapOrder(userId, toolName, preview, details) {
|
|
104
|
+
if (toolName !== "swap_solana_privately") return;
|
|
105
|
+
if (!preview || typeof preview !== "object") return;
|
|
106
|
+
if (!details || typeof details !== "object") return;
|
|
107
|
+
const houdiniId = typeof details.houdini_id === "string" ? details.houdini_id.trim() : "";
|
|
108
|
+
const depositAddress =
|
|
109
|
+
typeof details.deposit_address === "string" ? details.deposit_address.trim() : "";
|
|
110
|
+
if (!houdiniId || !depositAddress) return;
|
|
111
|
+
privateSwapOrderCache.set(approvalCacheKey(userId, toolName), {
|
|
112
|
+
digest: previewDigest(preview),
|
|
113
|
+
expiresAt: Date.now() + PRIVATE_SWAP_CACHE_TTL_MS,
|
|
114
|
+
order: {
|
|
115
|
+
multi_id: typeof details.multi_id === "string" ? details.multi_id.trim() : null,
|
|
116
|
+
houdini_id: houdiniId,
|
|
117
|
+
deposit_address: depositAddress,
|
|
118
|
+
order: details.order && typeof details.order === "object" ? details.order : {},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function latestPendingPrivateSwapOrder(userId, toolName, preview) {
|
|
124
|
+
if (toolName !== "swap_solana_privately") return null;
|
|
125
|
+
const cached = privateSwapOrderCache.get(approvalCacheKey(userId, toolName));
|
|
126
|
+
if (!cached || typeof cached !== "object") return null;
|
|
127
|
+
if (Number(cached.expiresAt || 0) <= Date.now()) {
|
|
128
|
+
privateSwapOrderCache.delete(approvalCacheKey(userId, toolName));
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (!preview || typeof preview !== "object") return null;
|
|
132
|
+
if (cached.digest !== previewDigest(preview)) return null;
|
|
133
|
+
return cached.order && typeof cached.order === "object" ? cached.order : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function clearPendingPrivateSwapOrder(userId, toolName) {
|
|
137
|
+
if (toolName !== "swap_solana_privately") return;
|
|
138
|
+
privateSwapOrderCache.delete(approvalCacheKey(userId, toolName));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatPrivateSwapPendingOrderError(details) {
|
|
142
|
+
const houdiniId = typeof details?.houdini_id === "string" ? details.houdini_id.trim() : "";
|
|
143
|
+
const multiId = typeof details?.multi_id === "string" ? details.multi_id.trim() : "";
|
|
144
|
+
const depositAddress =
|
|
145
|
+
typeof details?.deposit_address === "string" ? details.deposit_address.trim() : "";
|
|
146
|
+
const orderStatus =
|
|
147
|
+
typeof details?.order_status === "string" ? details.order_status.trim() : "";
|
|
148
|
+
const parts = [
|
|
149
|
+
"Houdini order was created, but the Solana deposit account is not ready yet.",
|
|
150
|
+
];
|
|
151
|
+
if (houdiniId) parts.push(`houdini_id=${houdiniId}`);
|
|
152
|
+
if (multiId) parts.push(`multi_id=${multiId}`);
|
|
153
|
+
if (depositAddress) parts.push(`deposit_address=${depositAddress}`);
|
|
154
|
+
if (orderStatus) parts.push(`status=${orderStatus}`);
|
|
155
|
+
parts.push("Retry execute for this existing order instead of generating a new preview.");
|
|
156
|
+
return parts.join(" ");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatPrivateSwapRateLimitError(details) {
|
|
160
|
+
const retryAfter =
|
|
161
|
+
typeof details?.retry_after === "number"
|
|
162
|
+
? details.retry_after
|
|
163
|
+
: typeof details?.retry_after === "string"
|
|
164
|
+
? details.retry_after
|
|
165
|
+
: "";
|
|
166
|
+
const quoteId = typeof details?.quote_id === "string" ? details.quote_id.trim() : "";
|
|
167
|
+
const parts = [
|
|
168
|
+
"Houdini exchange create is rate-limited right now.",
|
|
169
|
+
];
|
|
170
|
+
if (retryAfter !== "") parts.push(`retry_after=${retryAfter}s`);
|
|
171
|
+
if (quoteId) parts.push(`quote_id=${quoteId}`);
|
|
172
|
+
parts.push("Do not generate a new preview yet; wait, then retry execute.");
|
|
173
|
+
return parts.join(" ");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function listPendingPrivateSwapOrders(userId) {
|
|
177
|
+
const key = approvalCacheKey(userId, PRIVATE_SWAP_APPROVAL_TOOL_NAME);
|
|
178
|
+
const pending = privateSwapOrderCache.get(key);
|
|
179
|
+
if (!pending || typeof pending !== "object" || Number(pending.expiresAt || 0) <= Date.now()) {
|
|
180
|
+
privateSwapOrderCache.delete(key);
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
...(pending.order && typeof pending.order === "object" ? pending.order : {}),
|
|
186
|
+
expires_at_ms: Number(pending.expiresAt || 0),
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
}
|
|
13
190
|
|
|
14
191
|
function resolvePluginConfig(api) {
|
|
15
192
|
const globalConfig = api?.config ?? {};
|
|
@@ -183,9 +360,21 @@ function networkForBackend(api, backend) {
|
|
|
183
360
|
return selectedEvmNetwork || defaultSelectableEvmNetwork(api) || "ethereum";
|
|
184
361
|
}
|
|
185
362
|
if (backend === "wdk_btc_local") {
|
|
186
|
-
|
|
363
|
+
try {
|
|
364
|
+
return selectedBtcNetwork || defaultBtcNetwork(api);
|
|
365
|
+
} catch {
|
|
366
|
+
return "bitcoin";
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
return (
|
|
371
|
+
selectedSolanaNetwork ||
|
|
372
|
+
normalizeSolanaNetwork(config.network || process.env.SOLANA_NETWORK) ||
|
|
373
|
+
"mainnet"
|
|
374
|
+
);
|
|
375
|
+
} catch {
|
|
376
|
+
return "mainnet";
|
|
187
377
|
}
|
|
188
|
-
return selectedSolanaNetwork || normalizeSolanaNetwork(config.network || process.env.SOLANA_NETWORK) || "mainnet";
|
|
189
378
|
}
|
|
190
379
|
|
|
191
380
|
function effectiveConfigForBackend(api, backend) {
|
|
@@ -244,11 +433,37 @@ async function callWalletCli(api, command, extraArgs = [], configOverride = null
|
|
|
244
433
|
...extraArgs,
|
|
245
434
|
];
|
|
246
435
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
436
|
+
let stdout = "";
|
|
437
|
+
let stderr = "";
|
|
438
|
+
try {
|
|
439
|
+
const result = await execFileAsync(pythonBin, args, {
|
|
440
|
+
cwd: packageRoot,
|
|
441
|
+
env: buildCliEnv(packageRoot),
|
|
442
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
443
|
+
});
|
|
444
|
+
stdout = result.stdout;
|
|
445
|
+
stderr = result.stderr;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
stdout = typeof error?.stdout === "string" ? error.stdout : "";
|
|
448
|
+
stderr = typeof error?.stderr === "string" ? error.stderr : "";
|
|
449
|
+
const stderrText = String(stderr || "").trim();
|
|
450
|
+
if (stderrText) {
|
|
451
|
+
try {
|
|
452
|
+
const payload = JSON.parse(stderrText);
|
|
453
|
+
const wrapped = new Error(payload?.error || "agent-wallet CLI failed");
|
|
454
|
+
if (payload?.code) wrapped.code = payload.code;
|
|
455
|
+
if (payload?.details && typeof payload.details === "object") {
|
|
456
|
+
wrapped.details = payload.details;
|
|
457
|
+
}
|
|
458
|
+
throw wrapped;
|
|
459
|
+
} catch (parseError) {
|
|
460
|
+
if (parseError instanceof Error && parseError !== error) {
|
|
461
|
+
throw parseError;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
252
467
|
|
|
253
468
|
if (stderr && stderr.trim()) {
|
|
254
469
|
api?.logger?.debug?.(`[agent-wallet] stderr: ${stderr.trim()}`);
|
|
@@ -256,11 +471,43 @@ async function callWalletCli(api, command, extraArgs = [], configOverride = null
|
|
|
256
471
|
|
|
257
472
|
const payload = JSON.parse(stdout.trim() || "{}");
|
|
258
473
|
if (payload?.ok === false && payload?.error) {
|
|
259
|
-
|
|
474
|
+
const wrapped = new Error(payload.error);
|
|
475
|
+
if (payload?.error_code) wrapped.code = payload.error_code;
|
|
476
|
+
if (payload?.error_details && typeof payload.error_details === "object") {
|
|
477
|
+
wrapped.details = payload.error_details;
|
|
478
|
+
}
|
|
479
|
+
throw wrapped;
|
|
260
480
|
}
|
|
261
481
|
return payload;
|
|
262
482
|
}
|
|
263
483
|
|
|
484
|
+
async function issueApprovalToken(api, config, userId, toolName, previewPayload) {
|
|
485
|
+
const summary = previewPayload?.confirmation_summary;
|
|
486
|
+
if (!summary || typeof summary !== "object") {
|
|
487
|
+
throw new Error(`No confirmation_summary available for ${toolName}.`);
|
|
488
|
+
}
|
|
489
|
+
const digest = previewDigest(previewPayload);
|
|
490
|
+
const summaryForToken = { ...summary, _preview_digest: digest };
|
|
491
|
+
const extraArgs = [
|
|
492
|
+
"--tool",
|
|
493
|
+
toolName,
|
|
494
|
+
"--summary-json",
|
|
495
|
+
JSON.stringify(summaryForToken),
|
|
496
|
+
];
|
|
497
|
+
if (previewPayload?.is_mainnet === true) {
|
|
498
|
+
extraArgs.push("--mainnet-confirmed");
|
|
499
|
+
}
|
|
500
|
+
if (toolName === "swap_solana_privately") {
|
|
501
|
+
extraArgs.push("--ttl-seconds", "1800");
|
|
502
|
+
}
|
|
503
|
+
const payload = await callWalletCli(api, "issue-approval", extraArgs, config);
|
|
504
|
+
const token = String(payload?.approval_token || "").trim();
|
|
505
|
+
if (!token) {
|
|
506
|
+
throw new Error(`issue-approval did not return an approval_token for ${toolName}.`);
|
|
507
|
+
}
|
|
508
|
+
return token;
|
|
509
|
+
}
|
|
510
|
+
|
|
264
511
|
function asContent(data) {
|
|
265
512
|
return {
|
|
266
513
|
content: [
|
|
@@ -374,8 +621,15 @@ function registerTool(api, definition) {
|
|
|
374
621
|
});
|
|
375
622
|
}
|
|
376
623
|
|
|
624
|
+
if (definition.name === "list_pending_solana_private_swaps") {
|
|
625
|
+
return asContent({
|
|
626
|
+
orders: listPendingPrivateSwapOrders(resolveUserId(api, resolvePluginConfig(api))),
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
377
630
|
const effectiveParams = { ...(params ?? {}) };
|
|
378
631
|
const activeBackend = activeBackendForTool(api, definition.name);
|
|
632
|
+
const userId = resolveUserId(api, resolvePluginConfig(api));
|
|
379
633
|
if (
|
|
380
634
|
activeBackend === "wdk_evm_local" &&
|
|
381
635
|
selectedEvmNetwork &&
|
|
@@ -388,12 +642,138 @@ function registerTool(api, definition) {
|
|
|
388
642
|
if (activeBackend === "wdk_evm_local" && effectiveParams.network !== undefined) {
|
|
389
643
|
configOverride.network = normalizeSelectableEvmNetwork(effectiveParams.network);
|
|
390
644
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
645
|
+
if (String(effectiveParams.mode || "") === "execute") {
|
|
646
|
+
if (
|
|
647
|
+
PREVIEW_BOUND_SWAP_TOOLS.has(definition.name) &&
|
|
648
|
+
typeof effectiveParams.approval_token === "string" &&
|
|
649
|
+
effectiveParams.approval_token.trim() &&
|
|
650
|
+
effectiveParams._approved_preview === undefined
|
|
651
|
+
) {
|
|
652
|
+
const cachedPreview = cachedPreviewForToken(userId, definition.name, effectiveParams.approval_token);
|
|
653
|
+
if (cachedPreview) {
|
|
654
|
+
effectiveParams._approved_preview = cachedPreview;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (!effectiveParams.approval_token) {
|
|
658
|
+
const cached = latestCachedPreview(userId, definition.name);
|
|
659
|
+
if (cached?.preview && cached?.summary) {
|
|
660
|
+
effectiveParams.approval_token = await issueApprovalToken(
|
|
661
|
+
api,
|
|
662
|
+
configOverride,
|
|
663
|
+
userId,
|
|
664
|
+
definition.name,
|
|
665
|
+
cached.preview
|
|
666
|
+
);
|
|
667
|
+
if (PREVIEW_BOUND_SWAP_TOOLS.has(definition.name) && effectiveParams._approved_preview === undefined) {
|
|
668
|
+
effectiveParams._approved_preview = cached.preview;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (definition.name === "continue_solana_private_swap") {
|
|
674
|
+
const cached = latestCachedPreview(userId, PRIVATE_SWAP_APPROVAL_TOOL_NAME);
|
|
675
|
+
if (cached?.preview && effectiveParams._approved_preview === undefined) {
|
|
676
|
+
effectiveParams._approved_preview = cached.preview;
|
|
677
|
+
}
|
|
678
|
+
if (!effectiveParams.approval_token && cached?.preview && cached?.summary) {
|
|
679
|
+
effectiveParams.approval_token = await issueApprovalToken(
|
|
680
|
+
api,
|
|
681
|
+
configOverride,
|
|
682
|
+
userId,
|
|
683
|
+
PRIVATE_SWAP_APPROVAL_TOOL_NAME,
|
|
684
|
+
cached.preview
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
if (effectiveParams._resume_private_swap_order === undefined && cached?.preview) {
|
|
688
|
+
const pendingOrder = latestPendingPrivateSwapOrder(
|
|
689
|
+
userId,
|
|
690
|
+
PRIVATE_SWAP_APPROVAL_TOOL_NAME,
|
|
691
|
+
cached.preview
|
|
692
|
+
);
|
|
693
|
+
if (pendingOrder) {
|
|
694
|
+
if (
|
|
695
|
+
effectiveParams.houdini_id &&
|
|
696
|
+
pendingOrder.houdini_id &&
|
|
697
|
+
String(effectiveParams.houdini_id).trim() !== String(pendingOrder.houdini_id).trim()
|
|
698
|
+
) {
|
|
699
|
+
throw new Error("The requested houdini_id does not match the cached pending private swap order.");
|
|
700
|
+
}
|
|
701
|
+
effectiveParams._resume_private_swap_order = pendingOrder;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const executeWalletTool = async () =>
|
|
706
|
+
callWalletCli(api, "invoke", [
|
|
707
|
+
"--tool",
|
|
708
|
+
definition.name,
|
|
709
|
+
"--arguments-json",
|
|
710
|
+
JSON.stringify(effectiveParams),
|
|
711
|
+
], configOverride);
|
|
712
|
+
|
|
713
|
+
let payload;
|
|
714
|
+
if (definition.name === "swap_solana_privately" && String(effectiveParams.mode || "") === "execute") {
|
|
715
|
+
const approvedPreview =
|
|
716
|
+
effectiveParams._approved_preview && typeof effectiveParams._approved_preview === "object"
|
|
717
|
+
? effectiveParams._approved_preview
|
|
718
|
+
: null;
|
|
719
|
+
const pendingOrder = approvedPreview
|
|
720
|
+
? latestPendingPrivateSwapOrder(userId, definition.name, approvedPreview)
|
|
721
|
+
: null;
|
|
722
|
+
if (pendingOrder && effectiveParams._resume_private_swap_order === undefined) {
|
|
723
|
+
effectiveParams._resume_private_swap_order = pendingOrder;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let remainingRetries = 3;
|
|
727
|
+
while (true) {
|
|
728
|
+
try {
|
|
729
|
+
payload = await executeWalletTool();
|
|
730
|
+
const executionState = payload?.data?.execution_state;
|
|
731
|
+
if (executionState === "awaiting_deposit_funding" && approvedPreview) {
|
|
732
|
+
cachePendingPrivateSwapOrder(userId, definition.name, approvedPreview, payload.data);
|
|
733
|
+
} else {
|
|
734
|
+
clearPendingPrivateSwapOrder(userId, definition.name);
|
|
735
|
+
}
|
|
736
|
+
break;
|
|
737
|
+
} catch (error) {
|
|
738
|
+
const errorCode = typeof error?.code === "string" ? error.code : "";
|
|
739
|
+
const errorDetails =
|
|
740
|
+
error?.details && typeof error.details === "object" ? error.details : null;
|
|
741
|
+
if (
|
|
742
|
+
(errorCode === "houdini_deposit_not_ready" ||
|
|
743
|
+
errorCode === "houdini_order_initializing_timeout") &&
|
|
744
|
+
approvedPreview &&
|
|
745
|
+
errorDetails &&
|
|
746
|
+
remainingRetries > 0
|
|
747
|
+
) {
|
|
748
|
+
cachePendingPrivateSwapOrder(userId, definition.name, approvedPreview, errorDetails);
|
|
749
|
+
effectiveParams._resume_private_swap_order =
|
|
750
|
+
latestPendingPrivateSwapOrder(userId, definition.name, approvedPreview) || undefined;
|
|
751
|
+
remainingRetries -= 1;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (
|
|
755
|
+
(errorCode === "houdini_deposit_not_ready" ||
|
|
756
|
+
errorCode === "houdini_order_initializing_timeout") &&
|
|
757
|
+
errorDetails
|
|
758
|
+
) {
|
|
759
|
+
cachePendingPrivateSwapOrder(userId, definition.name, approvedPreview, errorDetails);
|
|
760
|
+
throw new Error(formatPrivateSwapPendingOrderError(errorDetails));
|
|
761
|
+
}
|
|
762
|
+
if (errorCode === "houdini_exchange_rate_limited" && errorDetails) {
|
|
763
|
+
throw new Error(formatPrivateSwapRateLimitError(errorDetails));
|
|
764
|
+
}
|
|
765
|
+
throw error;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
} else if (definition.name === "continue_solana_private_swap") {
|
|
769
|
+
payload = await executeWalletTool();
|
|
770
|
+
if (payload?.data?.execution_state === "funding_submitted") {
|
|
771
|
+
clearPendingPrivateSwapOrder(userId, PRIVATE_SWAP_APPROVAL_TOOL_NAME);
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
payload = await executeWalletTool();
|
|
775
|
+
}
|
|
776
|
+
cachePreviewForApproval(userId, definition.name, payload);
|
|
397
777
|
if (payload?.ok === false) {
|
|
398
778
|
throw new Error(payload?.error || `${definition.name} failed`);
|
|
399
779
|
}
|
|
@@ -415,7 +795,7 @@ const walletSessionToolDefinitions = [
|
|
|
415
795
|
},
|
|
416
796
|
{
|
|
417
797
|
name: "get_wallet_balance",
|
|
418
|
-
description:
|
|
798
|
+
description: `Get the active wallet overview. Solana and EVM return native assets, discovered token balances, per-asset USD values when available, and total_value_usd. Use set_wallet_backend first when the user asks to switch wallets. ${WALLET_TOOL_ONLY_GUIDANCE}`,
|
|
419
799
|
parameters: {
|
|
420
800
|
type: "object",
|
|
421
801
|
properties: {
|
|
@@ -456,6 +836,12 @@ const walletSessionToolDefinitions = [
|
|
|
456
836
|
];
|
|
457
837
|
|
|
458
838
|
const solanaToolDefinitions = [
|
|
839
|
+
{
|
|
840
|
+
name: "list_pending_solana_private_swaps",
|
|
841
|
+
description:
|
|
842
|
+
"List cached pending Houdini private Solana orders from this OpenClaw session, including houdini_id, multi_id, deposit_address, and the last known order payload.",
|
|
843
|
+
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
844
|
+
},
|
|
459
845
|
{
|
|
460
846
|
name: "get_wallet_capabilities",
|
|
461
847
|
description: "Describe the connected wallet backend, chain, and safety limits.",
|
|
@@ -468,7 +854,7 @@ const solanaToolDefinitions = [
|
|
|
468
854
|
},
|
|
469
855
|
{
|
|
470
856
|
name: "get_wallet_balance",
|
|
471
|
-
description:
|
|
857
|
+
description: `Get the wallet overview: native balance, discovered token balances, per-asset USD values when available, and total_value_usd. Solana token discovery uses RPC; pricing uses Jupiter rather than RPC. ${WALLET_TOOL_ONLY_GUIDANCE}`,
|
|
472
858
|
parameters: {
|
|
473
859
|
type: "object",
|
|
474
860
|
properties: {
|
|
@@ -524,7 +910,7 @@ const solanaToolDefinitions = [
|
|
|
524
910
|
},
|
|
525
911
|
{
|
|
526
912
|
name: "get_wallet_portfolio",
|
|
527
|
-
description:
|
|
913
|
+
description: `Get the Solana wallet portfolio. This is the detailed equivalent of get_wallet_balance and includes native SOL, non-zero SPL token accounts, USD pricing when available, and total_value_usd. ${WALLET_TOOL_ONLY_GUIDANCE}`,
|
|
528
914
|
parameters: {
|
|
529
915
|
type: "object",
|
|
530
916
|
properties: {
|
|
@@ -741,7 +1127,7 @@ const solanaToolDefinitions = [
|
|
|
741
1127
|
},
|
|
742
1128
|
{
|
|
743
1129
|
name: "swap_solana_tokens",
|
|
744
|
-
description:
|
|
1130
|
+
description: `Preview, prepare, or execute a Solana token swap via Jupiter. Prepare returns an execution plan only, and execute requires a host-issued approval token bound to the previewed operation. ${WALLET_TOOL_ONLY_GUIDANCE}`,
|
|
745
1131
|
optional: true,
|
|
746
1132
|
parameters: {
|
|
747
1133
|
type: "object",
|
|
@@ -759,6 +1145,56 @@ const solanaToolDefinitions = [
|
|
|
759
1145
|
additionalProperties: false,
|
|
760
1146
|
},
|
|
761
1147
|
},
|
|
1148
|
+
{
|
|
1149
|
+
name: "swap_solana_privately",
|
|
1150
|
+
description: `Preview or create a Solana private payout through Houdini's anonymous routing. The initial implementation supports same-token private payouts only, such as SOL->SOL or USDC->USDC. Use preview first, then execute after explicit approval. The first execute creates the Houdini order and returns its deposit address; use continue_solana_private_swap to submit the funding transfer. ${WALLET_TOOL_ONLY_GUIDANCE}`,
|
|
1151
|
+
optional: true,
|
|
1152
|
+
parameters: {
|
|
1153
|
+
type: "object",
|
|
1154
|
+
properties: {
|
|
1155
|
+
input_token: { type: "string" },
|
|
1156
|
+
output_token: { type: "string" },
|
|
1157
|
+
destination_address: { type: "string" },
|
|
1158
|
+
amount: { type: "number" },
|
|
1159
|
+
use_xmr: { type: "boolean" },
|
|
1160
|
+
mode: { type: "string", enum: ["preview", "execute"] },
|
|
1161
|
+
purpose: { type: "string" },
|
|
1162
|
+
user_intent: { type: "boolean" },
|
|
1163
|
+
approval_token: { type: "string" },
|
|
1164
|
+
},
|
|
1165
|
+
required: ["input_token", "output_token", "destination_address", "amount", "mode", "purpose"],
|
|
1166
|
+
additionalProperties: false,
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
name: "continue_solana_private_swap",
|
|
1171
|
+
description:
|
|
1172
|
+
"Continue a previously created Houdini private Solana payout and submit the funding transfer to the cached deposit address. Use this after swap_solana_privately execute has returned a pending order.",
|
|
1173
|
+
optional: true,
|
|
1174
|
+
parameters: {
|
|
1175
|
+
type: "object",
|
|
1176
|
+
properties: {
|
|
1177
|
+
houdini_id: { type: "string" },
|
|
1178
|
+
approval_token: { type: "string" },
|
|
1179
|
+
},
|
|
1180
|
+
required: ["approval_token"],
|
|
1181
|
+
additionalProperties: false,
|
|
1182
|
+
},
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
name: "get_solana_private_swap_status",
|
|
1186
|
+
description: "Check Houdini status for a Solana private payout created by swap_solana_privately. Prefer houdini_id from the execute result; multi_id is only needed for legacy multi-order flows.",
|
|
1187
|
+
optional: true,
|
|
1188
|
+
parameters: {
|
|
1189
|
+
type: "object",
|
|
1190
|
+
properties: {
|
|
1191
|
+
multi_id: { type: "string" },
|
|
1192
|
+
houdini_id: { type: "string" },
|
|
1193
|
+
},
|
|
1194
|
+
anyOf: [{ required: ["multi_id"] }, { required: ["houdini_id"] }],
|
|
1195
|
+
additionalProperties: false,
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
762
1198
|
{
|
|
763
1199
|
name: "swap_solana_lifi_cross_chain_tokens",
|
|
764
1200
|
description: "Preview, prepare, or execute a Solana-origin cross-chain swap through LI.FI. This currently supports Solana as the source chain and ethereum/base as the destination chain. Prepare returns an execution plan only, and execute requires a host-issued approval token bound to the previewed operation.",
|
|
@@ -3,6 +3,72 @@
|
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Official OpenClaw plugin bridge for the agent-wallet backends, including Solana, local BTC, and local EVM.",
|
|
5
5
|
"version": "0.1.0",
|
|
6
|
+
"contracts": {
|
|
7
|
+
"tools": [
|
|
8
|
+
"claim_bags_fees",
|
|
9
|
+
"close_empty_token_accounts",
|
|
10
|
+
"continue_solana_private_swap",
|
|
11
|
+
"get_active_wallet_backend",
|
|
12
|
+
"get_bags_claimable_positions",
|
|
13
|
+
"get_bags_fee_analytics",
|
|
14
|
+
"get_btc_fee_rates",
|
|
15
|
+
"get_btc_max_spendable",
|
|
16
|
+
"get_btc_transfer_history",
|
|
17
|
+
"get_evm_aave_account",
|
|
18
|
+
"get_evm_aave_positions",
|
|
19
|
+
"get_evm_aave_reserves",
|
|
20
|
+
"get_evm_fee_rates",
|
|
21
|
+
"get_evm_lido_overview",
|
|
22
|
+
"get_evm_lido_positions",
|
|
23
|
+
"get_evm_lido_withdrawal_requests",
|
|
24
|
+
"get_evm_network",
|
|
25
|
+
"get_evm_swap_quote",
|
|
26
|
+
"get_evm_token_balance",
|
|
27
|
+
"get_evm_token_metadata",
|
|
28
|
+
"get_evm_transaction_receipt",
|
|
29
|
+
"get_jupiter_earn_earnings",
|
|
30
|
+
"get_jupiter_earn_positions",
|
|
31
|
+
"get_jupiter_earn_tokens",
|
|
32
|
+
"get_kamino_lend_market_reserves",
|
|
33
|
+
"get_kamino_lend_markets",
|
|
34
|
+
"get_kamino_lend_user_obligations",
|
|
35
|
+
"get_kamino_lend_user_rewards",
|
|
36
|
+
"get_lifi_quote",
|
|
37
|
+
"get_lifi_supported_chains",
|
|
38
|
+
"get_lifi_transfer_status",
|
|
39
|
+
"get_solana_private_swap_status",
|
|
40
|
+
"get_solana_token_prices",
|
|
41
|
+
"get_wallet_address",
|
|
42
|
+
"get_wallet_balance",
|
|
43
|
+
"get_wallet_capabilities",
|
|
44
|
+
"get_wallet_portfolio",
|
|
45
|
+
"jupiter_earn_deposit",
|
|
46
|
+
"jupiter_earn_withdraw",
|
|
47
|
+
"kamino_lend_borrow",
|
|
48
|
+
"kamino_lend_deposit",
|
|
49
|
+
"kamino_lend_repay",
|
|
50
|
+
"kamino_lend_withdraw",
|
|
51
|
+
"launch_bags_token",
|
|
52
|
+
"list_pending_solana_private_swaps",
|
|
53
|
+
"manage_evm_aave_position",
|
|
54
|
+
"manage_evm_lido_position",
|
|
55
|
+
"manage_evm_lido_withdrawal",
|
|
56
|
+
"request_devnet_airdrop",
|
|
57
|
+
"set_evm_network",
|
|
58
|
+
"set_wallet_backend",
|
|
59
|
+
"sign_wallet_message",
|
|
60
|
+
"swap_evm_lifi_cross_chain_tokens",
|
|
61
|
+
"swap_evm_tokens",
|
|
62
|
+
"swap_solana_lifi_cross_chain_tokens",
|
|
63
|
+
"swap_solana_privately",
|
|
64
|
+
"swap_solana_tokens",
|
|
65
|
+
"transfer_btc",
|
|
66
|
+
"transfer_evm_native",
|
|
67
|
+
"transfer_evm_token",
|
|
68
|
+
"transfer_sol",
|
|
69
|
+
"transfer_spl_token"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
6
72
|
"skills": ["skills/wallet-operator"],
|
|
7
73
|
"configSchema": {
|
|
8
74
|
"type": "object",
|
|
@@ -171,6 +237,36 @@
|
|
|
171
237
|
"sensitive": true
|
|
172
238
|
}
|
|
173
239
|
},
|
|
240
|
+
"houdiniBaseUrl": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"description": "Optional Houdini Partner API base URL for private Solana payouts."
|
|
243
|
+
},
|
|
244
|
+
"houdiniApiKey": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"description": "Optional Houdini Partner API key.",
|
|
247
|
+
"uiHints": {
|
|
248
|
+
"sensitive": true
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
"houdiniApiSecret": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"description": "Optional Houdini Partner API secret.",
|
|
254
|
+
"uiHints": {
|
|
255
|
+
"sensitive": true
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
"houdiniUserIp": {
|
|
259
|
+
"type": "string",
|
|
260
|
+
"description": "Required Houdini compliance header: end-user IP address."
|
|
261
|
+
},
|
|
262
|
+
"houdiniUserAgent": {
|
|
263
|
+
"type": "string",
|
|
264
|
+
"description": "Required Houdini compliance header: end-user agent string."
|
|
265
|
+
},
|
|
266
|
+
"houdiniUserTimezone": {
|
|
267
|
+
"type": "string",
|
|
268
|
+
"description": "Required Houdini compliance header: end-user timezone, for example Europe/Moscow."
|
|
269
|
+
},
|
|
174
270
|
"kaminoBaseUrl": {
|
|
175
271
|
"type": "string",
|
|
176
272
|
"description": "Optional Kamino REST API base URL."
|
|
@@ -5,11 +5,13 @@ Use wallet tools only when the user explicitly asks for wallet information, sign
|
|
|
5
5
|
Safety rules:
|
|
6
6
|
|
|
7
7
|
- Prefer read-only tools first.
|
|
8
|
+
- When a wallet tool exists for the task, do not fall back to `exec`, `solana`, `spl-token`, `bitcoin-cli`, `curl`, or any shell-based wallet workflow. If the wallet tool fails, report the tool error and stop.
|
|
8
9
|
- Jupiter Portfolio tools are temporarily disabled. Do not suggest or call them until they are re-enabled.
|
|
9
10
|
- Use Jupiter Earn read tools before Jupiter Earn writes when the user needs lending/yield context.
|
|
10
11
|
- Use Kamino market/reserve reads before Kamino writes when the user needs lending context.
|
|
11
12
|
- Use Aave account reads before Aave writes when the user needs EVM lending context.
|
|
12
13
|
- For transfers, native staking, swaps, Aave writes, Jupiter Earn writes, and Kamino writes, use `preview` before `prepare` or `execute`.
|
|
14
|
+
- For `swap_solana_privately`, use `preview` and then `execute` after explicit user approval. Do not use `prepare` for this tool.
|
|
13
15
|
- Use `prepare` only when the user clearly intends to produce an execution plan.
|
|
14
16
|
- Use `execute` only after the host issues an `approval_token` bound to the exact previewed operation.
|
|
15
17
|
- On `mainnet`, require an approval token that includes explicit mainnet confirmation before any execution.
|