@agentlayer.tech/wallet 0.1.12 → 0.1.14
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/AGENTS.md +10 -1
- 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/.openclaw/extensions/pay-bridge/README.md +32 -0
- package/.openclaw/extensions/pay-bridge/core.mjs +287 -0
- package/.openclaw/extensions/pay-bridge/index.ts +196 -0
- package/.openclaw/extensions/pay-bridge/openclaw.plugin.json +34 -0
- package/.openclaw/extensions/pay-bridge/package.json +11 -0
- package/.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md +20 -0
- package/.openclaw/extensions/pay-bridge/smoke_pay_bridge.mjs +38 -0
- package/CHANGELOG.md +10 -0
- package/README.md +16 -2
- package/agent-wallet/.env.example +11 -0
- package/agent-wallet/README.md +29 -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/install_agent_wallet.py +54 -2
- package/agent-wallet/scripts/install_openclaw_local_config.py +128 -6
- package/hermes/plugins/agent_wallet/tools.py +93 -9
- package/package.json +2 -1
package/.openclaw/AGENTS.md
CHANGED
|
@@ -4,17 +4,25 @@
|
|
|
4
4
|
These instructions apply to the entire `.openclaw/` tree.
|
|
5
5
|
|
|
6
6
|
## Purpose
|
|
7
|
-
This tree contains local OpenClaw host-side workspace assets. In the current repo, its
|
|
7
|
+
This tree contains local OpenClaw host-side workspace assets. In the current repo, its main responsibilities are:
|
|
8
|
+
|
|
9
|
+
- the `agent-wallet` extension that bridges OpenClaw to the authoritative Python `agent-wallet` backend
|
|
10
|
+
- the `pay-bridge` extension that bridges OpenClaw to the local `pay` CLI for paid API discovery and execution
|
|
8
11
|
|
|
9
12
|
## Current structure
|
|
10
13
|
- `.openclaw/extensions/agent-wallet/index.ts` — TypeScript extension entrypoint registered by OpenClaw.
|
|
11
14
|
- `.openclaw/extensions/agent-wallet/openclaw.plugin.json` — plugin manifest and config schema.
|
|
12
15
|
- `.openclaw/extensions/agent-wallet/package.json` — extension package metadata.
|
|
13
16
|
- `.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md` — user-facing operational wallet safety guidance.
|
|
17
|
+
- `.openclaw/extensions/pay-bridge/index.ts` — TypeScript entrypoint for the local `pay` CLI bridge.
|
|
18
|
+
- `.openclaw/extensions/pay-bridge/openclaw.plugin.json` — plugin manifest and config schema for pay tools.
|
|
19
|
+
- `.openclaw/extensions/pay-bridge/core.mjs` — local `pay` command execution and output shaping.
|
|
20
|
+
- `.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md` — user-facing operational guidance for paid API usage.
|
|
14
21
|
|
|
15
22
|
## Design intent
|
|
16
23
|
- Keep the TypeScript extension thin and host-oriented.
|
|
17
24
|
- Let Python own wallet logic, policy, approvals, signing rules, and Solana implementation details.
|
|
25
|
+
- Let `pay` remain the source of truth for paid API wallet/account behavior.
|
|
18
26
|
- Let the extension focus on:
|
|
19
27
|
- resolving config
|
|
20
28
|
- locating the Python package
|
|
@@ -27,6 +35,7 @@ This tree contains local OpenClaw host-side workspace assets. In the current rep
|
|
|
27
35
|
|
|
28
36
|
### Keep bridge logic thin
|
|
29
37
|
- Do not duplicate business logic from Python unless OpenClaw requires it at registration time.
|
|
38
|
+
- Do not duplicate payment protocol logic from `pay`; prefer invoking the local CLI and shaping its output.
|
|
30
39
|
- Do not reimplement approval validation, transaction policy, wallet derivation, or Solana-specific rules in TypeScript.
|
|
31
40
|
- Prefer forwarding config into the CLI bridge and letting Python decide runtime behavior.
|
|
32
41
|
- Treat this layer as a transport and schema bridge, not an execution authority.
|
|
@@ -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.",
|