@agentlayer.tech/wallet 0.1.12 → 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.
@@ -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
- return selectedBtcNetwork || defaultBtcNetwork(api);
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
- const { stdout, stderr } = await execFileAsync(pythonBin, args, {
248
- cwd: packageRoot,
249
- env: buildCliEnv(packageRoot),
250
- maxBuffer: 1024 * 1024 * 8,
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
- throw new Error(payload.error);
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
- const payload = await callWalletCli(api, "invoke", [
392
- "--tool",
393
- definition.name,
394
- "--arguments-json",
395
- JSON.stringify(effectiveParams),
396
- ], configOverride);
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: "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.",
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: "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.",
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: "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.",
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: "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.",
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.