@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.
Files changed (31) hide show
  1. package/.openclaw/AGENTS.md +10 -1
  2. package/.openclaw/extensions/agent-wallet/index.ts +454 -18
  3. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +96 -0
  4. package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +2 -0
  5. package/.openclaw/extensions/pay-bridge/README.md +32 -0
  6. package/.openclaw/extensions/pay-bridge/core.mjs +287 -0
  7. package/.openclaw/extensions/pay-bridge/index.ts +196 -0
  8. package/.openclaw/extensions/pay-bridge/openclaw.plugin.json +34 -0
  9. package/.openclaw/extensions/pay-bridge/package.json +11 -0
  10. package/.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md +20 -0
  11. package/.openclaw/extensions/pay-bridge/smoke_pay_bridge.mjs +38 -0
  12. package/CHANGELOG.md +10 -0
  13. package/README.md +16 -2
  14. package/agent-wallet/.env.example +11 -0
  15. package/agent-wallet/README.md +29 -0
  16. package/agent-wallet/agent_wallet/approval.py +4 -0
  17. package/agent-wallet/agent_wallet/config.py +6 -0
  18. package/agent-wallet/agent_wallet/exceptions.py +2 -1
  19. package/agent-wallet/agent_wallet/openclaw_adapter.py +361 -2
  20. package/agent-wallet/agent_wallet/openclaw_cli.py +13 -1
  21. package/agent-wallet/agent_wallet/openclaw_runtime.py +2 -5
  22. package/agent-wallet/agent_wallet/providers/houdini.py +539 -0
  23. package/agent-wallet/agent_wallet/transaction_policy.py +251 -0
  24. package/agent-wallet/agent_wallet/user_wallets.py +83 -0
  25. package/agent-wallet/agent_wallet/wallet_layer/base.py +40 -0
  26. package/agent-wallet/agent_wallet/wallet_layer/solana.py +885 -16
  27. package/agent-wallet/pyproject.toml +1 -1
  28. package/agent-wallet/scripts/install_agent_wallet.py +54 -2
  29. package/agent-wallet/scripts/install_openclaw_local_config.py +128 -6
  30. package/hermes/plugins/agent_wallet/tools.py +93 -9
  31. package/package.json +2 -1
@@ -0,0 +1,20 @@
1
+ # pay-operator
2
+
3
+ Use this skill when the user wants to discover or call paid APIs through `pay`.
4
+
5
+ ## Rules
6
+
7
+ - Treat the `pay` wallet as separate from the AgentLayer execution wallet.
8
+ - Do not use `agent-wallet` tools for `pay` account management.
9
+ - Do not fall back to shell commands when the `pay-bridge` tools exist.
10
+ - Prefer this order:
11
+ 1. `pay_status`
12
+ 2. `pay_search_services`
13
+ 3. `pay_get_service_endpoints`
14
+ 4. `pay_api_request`
15
+
16
+ ## Notes
17
+
18
+ - `pay_api_request` requires `purpose` and `user_confirmed=true`.
19
+ - Use the exact gateway URL returned by `pay_get_service_endpoints`.
20
+ - If `pay_status` shows no configured account, stop and ask the user to finish `pay setup`.
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict";
2
+
3
+ import {
4
+ endpointPayloadContainsUrl,
5
+ parseAccountListOutput,
6
+ parseWhoamiOutput,
7
+ } from "./core.mjs";
8
+
9
+ const whoami = parseWhoamiOutput(`
10
+ yuriytsygankov
11
+ \u001b[2m(no mainnet account — run \`pay setup\`)\u001b[0m
12
+ `);
13
+ assert.equal(whoami.system_user, "yuriytsygankov");
14
+ assert.equal(whoami.has_mainnet_account, false);
15
+
16
+ const accounts = parseAccountListOutput(`
17
+ \u001b[2mNo accounts found. Run \`pay account new\` to create one.\u001b[0m
18
+ `);
19
+ assert.equal(accounts.has_accounts, false);
20
+
21
+ const endpointPayload = {
22
+ endpoints: [
23
+ {
24
+ method: "POST",
25
+ url: "https://api.example.com/v1/invoke",
26
+ },
27
+ ],
28
+ };
29
+ assert.equal(
30
+ endpointPayloadContainsUrl(endpointPayload, "https://api.example.com/v1/invoke"),
31
+ true
32
+ );
33
+ assert.equal(
34
+ endpointPayloadContainsUrl(endpointPayload, "https://api.example.com/v1/other"),
35
+ false
36
+ );
37
+
38
+ console.log("smoke_pay_bridge: ok");
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.1.14 - 2026-05-13
6
+
7
+ - Added a separate `.openclaw/extensions/pay-bridge/` plugin that keeps
8
+ `pay.sh` API payments outside the main AgentLayer execution wallet stack.
9
+ - Added OpenClaw tools for local `pay` discovery and execution:
10
+ `pay_status`, `pay_wallet_info`, `pay_search_services`,
11
+ `pay_get_service_endpoints`, and `pay_api_request`.
12
+ - Updated the local OpenClaw installer/runtime config flow to package and
13
+ enable the `pay-bridge` plugin alongside `agent-wallet`, including its
14
+ tool allowlist and absolute `pay` binary path when available.
5
15
  - Added an optional Hermes Agent bridge plugin under `hermes/plugins/agent_wallet`
6
16
  that forwards into the existing Python wallet CLI instead of duplicating
7
17
  OpenClaw wallet tools or policy.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ![AgentLayer](logo+name.png)
2
- # AgentLayer
2
+
3
+
4
+ [![npm version](https://img.shields.io/npm/v/%40agentlayer.tech%2Fwallet)](https://www.npmjs.com/package/@agentlayer.tech/wallet)
5
+ [![npm downloads](https://img.shields.io/npm/dm/%40agentlayer.tech%2Fwallet)](https://www.npmjs.com/package/@agentlayer.tech/wallet)
6
+ [![license](https://img.shields.io/github/license/lopushok9/Agent-Layer)](https://github.com/lopushok9/Agent-Layer/blob/main/LICENSE)
3
7
 
4
8
  ```bash
5
9
  npx @agentlayer.tech/wallet install --yes
@@ -14,7 +18,7 @@ AgentLayer is a beta local-first wallet and finance stack for agents.
14
18
  The repository includes:
15
19
 
16
20
  - `agent-wallet/` - the main wallet backend for AgentLayer
17
- - `.openclaw/` - the local AgentLayer bridge layer
21
+ - `.openclaw/` - the local AgentLayer bridge layer, including the OpenClaw wallet bridge and the `pay.sh` API-payments bridge
18
22
  - `hermes/` - optional Hermes Agent plugin bridge for the same wallet backend
19
23
  - `wdk-btc-wallet/` - the local Bitcoin wallet service
20
24
  - `wdk-evm-wallet/` - the local EVM wallet service
@@ -193,6 +197,16 @@ For Solana specifically, install alone does not make signed transactions availab
193
197
  - read-only mode: `SOLANA_AGENT_PUBLIC_KEY`
194
198
  - signing mode: a sealed `private_key` or `SOLANA_AGENT_KEYPAIR_PATH`
195
199
 
200
+ Optional private Solana payout routing is also available through Houdini. To enable it, add the Houdini partner credentials to the wallet runtime:
201
+
202
+ - `HOUDINI_API_KEY`
203
+ - `HOUDINI_API_SECRET`
204
+ - `HOUDINI_USER_IP`
205
+
206
+ The first supported flow is intentionally narrow: same-token private Solana payouts (`SOL->SOL` and `USDC->USDC`) through the existing preview/prepare/execute safety model. The runtime binds execute to the approved Houdini `quoteId`, creates a single private exchange, and then sends the exact deposit locally from the wallet. That removes the extra Solana batch-tx relay step and keeps signing local.
207
+
208
+ For production, prefer placing the Houdini partner secrets on `provider-gateway` and exposing only the authenticated Houdini relay endpoints to `agent-wallet`. That keeps `HOUDINI_API_KEY` and `HOUDINI_API_SECRET` out of end-user runtimes while preserving local signing.
209
+
196
210
  ## BTC setup
197
211
 
198
212
  The BTC path already has a one-command host bootstrap wrapper:
@@ -54,6 +54,17 @@ LIFI_API_KEY=
54
54
  LIFI_INTEGRATOR=openclaw
55
55
  LIFI_DEFAULT_DENY_BRIDGES=mayan
56
56
 
57
+ # Houdini Partner API for private Solana payouts.
58
+ # HOUDINI_USER_IP is required because Houdini rejects requests without compliance headers.
59
+ # If PROVIDER_GATEWAY_URL points at a gateway with Houdini enabled,
60
+ # the wallet runtime can omit these partner secrets and route through the gateway instead.
61
+ HOUDINI_API_BASE_URL=https://api-partner.houdiniswap.com/v2
62
+ HOUDINI_API_KEY=
63
+ HOUDINI_API_SECRET=
64
+ HOUDINI_USER_IP=
65
+ HOUDINI_USER_AGENT=AgentLayer/0.1.12
66
+ HOUDINI_USER_TIMEZONE=UTC
67
+
57
68
  # Kamino REST lending
58
69
  KAMINO_API_BASE_URL=https://api.kamino.finance
59
70
  KAMINO_PROGRAM_ID=KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD
@@ -89,6 +89,8 @@ Current safe tools:
89
89
  - `stake_sol_native`
90
90
  - `transfer_spl_token`
91
91
  - `swap_solana_tokens`
92
+ - `swap_solana_privately` - Houdini-backed private Solana payout flow for same-token `SOL->SOL` or `USDC->USDC` transfers to a destination wallet.
93
+ - `get_solana_private_swap_status`
92
94
  - `get_jupiter_earn_tokens`
93
95
  - `get_jupiter_earn_positions`
94
96
  - `get_jupiter_earn_earnings`
@@ -116,6 +118,8 @@ Temporarily disabled but kept in the codebase for later re-enable:
116
118
  The signing tool still requires explicit `user_confirmed=true`.
117
119
  Transfer, native staking, swap, and Aave position-management tools support `preview`, `prepare`, and `execute` modes. The safe operational path is still preview-first. `prepare` now returns an execution plan only and never exposes signed transaction bytes to the agent. `execute` works only when the backend has a signer and `sign_only=false`.
118
120
 
121
+ Exception: `swap_solana_privately` is intentionally optimized for `preview -> execute`. Hosts should not insert a separate `prepare` step for Houdini private payouts because it adds no execution value and only burns additional provider quota.
122
+
119
123
  Policy defaults:
120
124
 
121
125
  - read-only tools are always allowed
@@ -185,6 +189,30 @@ For production `mainnet`, prefer a dedicated RPC instead of the public Solana en
185
189
 
186
190
  Production recommendation: treat RPC as deployment-owned config, not wallet logic. Runtime env wins over `openclaw.json` plugin config, so keep `Alchemy/Helius/QuickNode` endpoints in deployment secrets or service env and use plugin `rpcUrl` / `rpcUrls` only as local fallback.
187
191
 
192
+ For Houdini-backed private Solana payouts, also provide:
193
+
194
+ - `HOUDINI_API_KEY`
195
+ - `HOUDINI_API_SECRET`
196
+ - `HOUDINI_USER_IP`
197
+ - optional `HOUDINI_USER_AGENT`
198
+ - optional `HOUDINI_USER_TIMEZONE`
199
+
200
+ The current MVP intentionally keeps the scope narrow:
201
+
202
+ - supported private routes are same-token Solana payouts only
203
+ - `SOL -> SOL`
204
+ - `USDC -> USDC`
205
+ - execution binds to the approved Houdini `quoteId`, creates a single private exchange, and sends the exact Solana deposit locally from the wallet
206
+
207
+ This is a private payout flow expressed in Houdini's swap terminology. Cross-token private swaps can be added later without changing the OpenClaw/Hermes approval model.
208
+
209
+ For production, the cleaner setup is to place Houdini partner secrets on `provider-gateway` and let `agent-wallet` consume the narrow gateway endpoints through `PROVIDER_GATEWAY_URL` and optional `PROVIDER_GATEWAY_BEARER_TOKEN`. In that mode:
210
+
211
+ - the gateway owns `HOUDINI_API_KEY` / `HOUDINI_API_SECRET`
212
+ - the gateway derives the authoritative user IP from ingress
213
+ - `agent-wallet` still performs preview/prepare/execute, local transaction verification, signing, and broadcast
214
+ - direct Houdini env vars can be omitted from the wallet runtime
215
+
188
216
  For OpenClaw onboarding, `agent-wallet` now ships with a hosted default provider gateway:
189
217
 
190
218
  - `https://agent-layer-production.up.railway.app`
@@ -536,6 +564,7 @@ Public-safe helper scripts are available in `agent-wallet/scripts/`:
536
564
  - `finalize_openclaw_local_wallet_config.py`
537
565
 
538
566
  Both scripts now use generic defaults instead of hardcoded local usernames or paths. Sensitive secrets must be supplied via protected environment variables, not config JSON or CLI arguments.
567
+ When `~/.openclaw/agent-wallet-runtime/current` exists, the config installer now prefers that trusted runtime path over a workspace checkout for the plugin manifest, package root, and Python bridge launcher.
539
568
 
540
569
  Recommended devnet setup:
541
570
 
@@ -3,9 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import base64
6
+ import functools
6
7
  import hashlib
7
8
  import hmac
8
9
  import json
10
+ import secrets
9
11
  import time
10
12
  from typing import Any
11
13
 
@@ -33,6 +35,7 @@ def _sign_payload(payload: dict[str, Any], secret: str) -> str:
33
35
  return _urlsafe_b64(digest)
34
36
 
35
37
 
38
+ @functools.lru_cache(maxsize=1)
36
39
  def _approval_secret() -> str:
37
40
  secret = resolve_approval_secret().strip()
38
41
  if not secret:
@@ -70,6 +73,7 @@ def issue_approval_token(
70
73
  "v": APPROVAL_TOKEN_VERSION,
71
74
  "iat": now,
72
75
  "exp": now + ttl,
76
+ "jti": secrets.token_urlsafe(16),
73
77
  "issued_by": issued_by,
74
78
  "binding": build_operation_binding(tool_name=tool_name, network=network, summary=summary),
75
79
  "mainnet_confirmed": bool(mainnet_confirmed),
@@ -54,6 +54,12 @@ class Settings(BaseSettings):
54
54
  lifi_api_key: str = ""
55
55
  lifi_integrator: str = "openclaw"
56
56
  lifi_default_deny_bridges: str = "mayan"
57
+ houdini_api_base_url: str = "https://api-partner.houdiniswap.com/v2"
58
+ houdini_api_key: str = ""
59
+ houdini_api_secret: str = ""
60
+ houdini_user_ip: str = ""
61
+ houdini_user_agent: str = "AgentLayer/0.1.12"
62
+ houdini_user_timezone: str = "UTC"
57
63
  kamino_api_base_url: str = "https://api.kamino.finance"
58
64
  kamino_program_id: str = "KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD"
59
65
  alchemy_api_key: str = ""
@@ -4,6 +4,7 @@
4
4
  class ProviderError(Exception):
5
5
  """A provider failed to return data."""
6
6
 
7
- def __init__(self, provider: str, message: str):
7
+ def __init__(self, provider: str, message: str, *, details: dict | None = None):
8
8
  self.provider = provider
9
+ self.details = dict(details) if isinstance(details, dict) else None
9
10
  super().__init__(f"[{provider}] {message}")
@@ -265,6 +265,27 @@ class OpenClawWalletAdapter:
265
265
  "transaction_data_hash": payload.get("transaction_data_hash"),
266
266
  }
267
267
 
268
+ if asset_type == "solana-private-swap":
269
+ return {
270
+ "operation": action_label,
271
+ "network": str(payload.get("network") or getattr(self.backend, "network", "unknown")),
272
+ "swap_provider": payload.get("source") or "houdini",
273
+ "owner": payload.get("owner"),
274
+ "destination_address": payload.get("destination_address"),
275
+ "input_token_id": payload.get("input_token_id"),
276
+ "output_token_id": payload.get("output_token_id"),
277
+ "input_token_symbol": payload.get("input_token_symbol"),
278
+ "output_token_symbol": payload.get("output_token_symbol"),
279
+ "input_token_address": payload.get("input_token_address"),
280
+ "output_token_address": payload.get("output_token_address"),
281
+ "input_amount_ui": payload.get("input_amount_ui"),
282
+ "estimated_output_amount_ui": payload.get("estimated_output_amount_ui"),
283
+ "private_duration_minutes": payload.get("private_duration_minutes"),
284
+ "quote_id": payload.get("quote_id"),
285
+ "anonymous": payload.get("anonymous"),
286
+ "use_xmr": payload.get("use_xmr"),
287
+ }
288
+
268
289
  if asset_type == "evm-lifi-cross-chain-swap":
269
290
  return {
270
291
  "operation": action_label,
@@ -644,8 +665,8 @@ class OpenClawWalletAdapter:
644
665
  )
645
666
  annotated["confirmation_requirements"] = {
646
667
  "prepare_requires_user_intent": mode == "prepare",
647
- "execute_requires_approval_token": mode == "execute",
648
- "execute_requires_mainnet_confirmed_in_token": mode == "execute" and is_mainnet,
668
+ "execute_requires_approval_token": True,
669
+ "execute_requires_mainnet_confirmed_in_token": is_mainnet,
649
670
  }
650
671
  if mode == "preview":
651
672
  annotated["approval_hint"] = {
@@ -2154,6 +2175,112 @@ class OpenClawWalletAdapter:
2154
2175
  )
2155
2176
  )
2156
2177
 
2178
+ tools.append(
2179
+ AgentToolSpec(
2180
+ name="swap_solana_privately",
2181
+ description=(
2182
+ "Preview or create a Solana private payout through Houdini's anonymous routing. "
2183
+ "The initial implementation supports same-token private payouts only, such as SOL->SOL or USDC->USDC. "
2184
+ "Use preview first, then execute after explicit approval. "
2185
+ "The first execute creates the Houdini order and returns the deposit address; use continue_solana_private_swap to submit the funding transfer."
2186
+ ),
2187
+ input_schema={
2188
+ "type": "object",
2189
+ "properties": {
2190
+ "input_token": {
2191
+ "type": "string",
2192
+ "description": "Source Solana token identifier. Symbol, name, mint address, or Houdini token id.",
2193
+ },
2194
+ "output_token": {
2195
+ "type": "string",
2196
+ "description": "Destination Solana token identifier. For the initial implementation, this must resolve to the same token as input_token.",
2197
+ },
2198
+ "destination_address": {
2199
+ "type": "string",
2200
+ "description": "Destination Solana wallet address that should receive the privately routed payout.",
2201
+ },
2202
+ "amount": {
2203
+ "type": "number",
2204
+ "description": "Input token amount in UI units.",
2205
+ },
2206
+ "use_xmr": {
2207
+ "type": "boolean",
2208
+ "description": "Optional. Force Houdini's XMR privacy hop when available.",
2209
+ },
2210
+ "mode": {
2211
+ "type": "string",
2212
+ "enum": ["preview", "execute"],
2213
+ },
2214
+ "purpose": {"type": "string"},
2215
+ "user_intent": {"type": "boolean"},
2216
+ "approval_token": {"type": "string"},
2217
+ },
2218
+ "required": [
2219
+ "input_token",
2220
+ "output_token",
2221
+ "destination_address",
2222
+ "amount",
2223
+ "mode",
2224
+ "purpose",
2225
+ ],
2226
+ "additionalProperties": False,
2227
+ },
2228
+ read_only=False,
2229
+ requires_explicit_user_intent=True,
2230
+ risk_level="high",
2231
+ )
2232
+ )
2233
+
2234
+ tools.append(
2235
+ AgentToolSpec(
2236
+ name="continue_solana_private_swap",
2237
+ description=(
2238
+ "Continue a previously created Houdini Solana private payout and submit the funding transfer "
2239
+ "to the saved deposit address. Use this only after swap_solana_privately execute has returned "
2240
+ "a pending order with deposit address details."
2241
+ ),
2242
+ input_schema={
2243
+ "type": "object",
2244
+ "properties": {
2245
+ "houdini_id": {
2246
+ "type": "string",
2247
+ "description": "Optional Houdini order id for the pending private payout. If omitted, the host may use the latest cached pending order.",
2248
+ },
2249
+ "approval_token": {
2250
+ "type": "string",
2251
+ "description": "Approval token issued from the original private swap preview.",
2252
+ },
2253
+ },
2254
+ "required": ["approval_token"],
2255
+ "additionalProperties": False,
2256
+ },
2257
+ read_only=False,
2258
+ requires_explicit_user_intent=True,
2259
+ risk_level="high",
2260
+ )
2261
+ )
2262
+
2263
+ tools.append(
2264
+ AgentToolSpec(
2265
+ name="get_solana_private_swap_status",
2266
+ description=(
2267
+ "Check Houdini status for a Solana private payout created by swap_solana_privately. "
2268
+ "Use houdini_id from the execute result. multi_id is still accepted for legacy multi-order flows."
2269
+ ),
2270
+ input_schema={
2271
+ "type": "object",
2272
+ "properties": {
2273
+ "multi_id": {"type": "string"},
2274
+ "houdini_id": {"type": "string"},
2275
+ },
2276
+ "anyOf": [{"required": ["multi_id"]}, {"required": ["houdini_id"]}],
2277
+ "additionalProperties": False,
2278
+ },
2279
+ read_only=True,
2280
+ risk_level="low",
2281
+ )
2282
+ )
2283
+
2157
2284
  tools.append(
2158
2285
  AgentToolSpec(
2159
2286
  name="claim_bags_fees",
@@ -4490,6 +4617,238 @@ class OpenClawWalletAdapter:
4490
4617
  ),
4491
4618
  )
4492
4619
 
4620
+ if tool_name == "swap_solana_privately":
4621
+ input_token = args.get("input_token")
4622
+ output_token = args.get("output_token")
4623
+ destination_address = args.get("destination_address")
4624
+ amount = args.get("amount")
4625
+ use_xmr = args.get("use_xmr", False)
4626
+ mode = args.get("mode")
4627
+ purpose = args.get("purpose")
4628
+ user_intent = args.get("user_intent", False)
4629
+ approval_token = args.get("approval_token")
4630
+
4631
+ if not isinstance(input_token, str) or not input_token.strip():
4632
+ raise WalletBackendError("input_token is required.")
4633
+ if not isinstance(output_token, str) or not output_token.strip():
4634
+ raise WalletBackendError("output_token is required.")
4635
+ if not isinstance(destination_address, str) or not destination_address.strip():
4636
+ raise WalletBackendError("destination_address is required.")
4637
+ if not isinstance(amount, (int, float)) or amount <= 0:
4638
+ raise WalletBackendError("amount must be a positive number.")
4639
+ if not isinstance(use_xmr, bool):
4640
+ raise WalletBackendError("use_xmr must be a boolean when provided.")
4641
+ if mode not in {"preview", "prepare", "execute"}:
4642
+ raise WalletBackendError("mode must be 'preview', 'prepare' or 'execute'.")
4643
+ if not isinstance(purpose, str) or not purpose.strip():
4644
+ raise WalletBackendError("purpose is required.")
4645
+
4646
+ preview_kwargs = {
4647
+ "input_token": input_token.strip(),
4648
+ "output_token": output_token.strip(),
4649
+ "destination_address": destination_address.strip(),
4650
+ "amount_ui": float(amount),
4651
+ "use_xmr": use_xmr,
4652
+ }
4653
+
4654
+ if mode == "preview":
4655
+ preview = await self.backend.preview_solana_private_swap(**preview_kwargs)
4656
+ return AgentToolResult(
4657
+ tool=tool_name,
4658
+ ok=True,
4659
+ data=self._annotate_sensitive_payload(
4660
+ preview,
4661
+ action_label="Solana private swap",
4662
+ mode="preview",
4663
+ ),
4664
+ )
4665
+
4666
+ if mode == "prepare":
4667
+ self._require_prepare_intent(user_intent)
4668
+ preview = await self.backend.preview_solana_private_swap(**preview_kwargs)
4669
+ return AgentToolResult(
4670
+ tool=tool_name,
4671
+ ok=True,
4672
+ data=self._annotate_sensitive_payload(
4673
+ self._build_prepare_plan(
4674
+ preview_payload=preview,
4675
+ action_label="Solana private swap",
4676
+ ),
4677
+ action_label="Solana private swap",
4678
+ mode="prepare",
4679
+ ),
4680
+ )
4681
+
4682
+ approval_payload = inspect_approval_token(
4683
+ approval_token,
4684
+ tool_name=tool_name,
4685
+ network=str(getattr(self.backend, "network", "unknown")),
4686
+ require_mainnet_confirmation=self._is_mainnet_for_backend(self.backend),
4687
+ )
4688
+ approval_summary = approval_payload.get("binding", {}).get("summary")
4689
+ if not isinstance(approval_summary, dict):
4690
+ raise WalletBackendError(
4691
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4692
+ )
4693
+ expected_summary = {
4694
+ "operation": "Solana private swap",
4695
+ "network": str(getattr(self.backend, "network", "unknown")),
4696
+ "destination_address": destination_address.strip(),
4697
+ "use_xmr": use_xmr,
4698
+ }
4699
+ for key, expected_value in expected_summary.items():
4700
+ if approval_summary.get(key) != expected_value:
4701
+ raise WalletBackendError(
4702
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4703
+ )
4704
+ try:
4705
+ approved_amount = float(approval_summary.get("input_amount_ui"))
4706
+ except (TypeError, ValueError):
4707
+ raise WalletBackendError(
4708
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4709
+ )
4710
+ if approved_amount != float(amount):
4711
+ raise WalletBackendError(
4712
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4713
+ )
4714
+
4715
+ approval_summary_copy = dict(approval_summary)
4716
+ approved_preview = args.get("_approved_preview")
4717
+ resume_private_swap_order = args.get("_resume_private_swap_order")
4718
+ if resume_private_swap_order is not None and not isinstance(resume_private_swap_order, dict):
4719
+ raise WalletBackendError("_resume_private_swap_order must be an object when provided.")
4720
+ execute_preview = None
4721
+ if isinstance(approval_summary_copy.get("_preview_digest"), str):
4722
+ if not isinstance(approved_preview, dict):
4723
+ raise WalletBackendError(
4724
+ "Approved private swap preview payload is required for execute mode. Generate a new preview and approval before execute."
4725
+ )
4726
+ if preview_payload_digest(approved_preview) != approval_summary_copy["_preview_digest"]:
4727
+ raise WalletBackendError(
4728
+ "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
4729
+ )
4730
+ preview_summary = self._build_confirmation_summary(
4731
+ action_label="Solana private swap",
4732
+ payload=approved_preview,
4733
+ )
4734
+ summary_without_digest = {
4735
+ key: value
4736
+ for key, value in approval_summary_copy.items()
4737
+ if key != "_preview_digest"
4738
+ }
4739
+ if preview_summary != summary_without_digest:
4740
+ raise WalletBackendError(
4741
+ "approved preview payload does not match the approval token. Generate a new preview and approval before execute."
4742
+ )
4743
+ execute_preview = dict(approved_preview)
4744
+
4745
+ self._require_execute_approval(
4746
+ approval_token=approval_token,
4747
+ tool_name=tool_name,
4748
+ summary=approval_summary_copy,
4749
+ action_label="Solana private swap",
4750
+ )
4751
+
4752
+ result = await self.backend.execute_solana_private_swap(
4753
+ **preview_kwargs,
4754
+ approved_preview=execute_preview,
4755
+ existing_order=resume_private_swap_order,
4756
+ )
4757
+ return AgentToolResult(
4758
+ tool=tool_name,
4759
+ ok=True,
4760
+ data=self._annotate_sensitive_payload(
4761
+ result,
4762
+ action_label="Solana private swap",
4763
+ mode="execute",
4764
+ ),
4765
+ )
4766
+
4767
+ if tool_name == "continue_solana_private_swap":
4768
+ approval_token = args.get("approval_token")
4769
+ approved_preview = args.get("_approved_preview")
4770
+ resume_private_swap_order = args.get("_resume_private_swap_order")
4771
+ if not isinstance(approved_preview, dict):
4772
+ raise WalletBackendError(
4773
+ "Approved private swap preview payload is required. Create the private swap order first."
4774
+ )
4775
+ if not isinstance(resume_private_swap_order, dict) or not resume_private_swap_order:
4776
+ raise WalletBackendError(
4777
+ "A pending Houdini private swap order is required. Create the private swap order first."
4778
+ )
4779
+
4780
+ approval_payload = inspect_approval_token(
4781
+ approval_token,
4782
+ tool_name="swap_solana_privately",
4783
+ network=str(getattr(self.backend, "network", "unknown")),
4784
+ require_mainnet_confirmation=self._is_mainnet_for_backend(self.backend),
4785
+ )
4786
+ approval_summary = approval_payload.get("binding", {}).get("summary")
4787
+ if not isinstance(approval_summary, dict):
4788
+ raise WalletBackendError(
4789
+ "approval_token does not match the requested private swap. Generate a new approval from the preview first."
4790
+ )
4791
+
4792
+ approval_summary_copy = dict(approval_summary)
4793
+ if isinstance(approval_summary_copy.get("_preview_digest"), str):
4794
+ if preview_payload_digest(approved_preview) != approval_summary_copy["_preview_digest"]:
4795
+ raise WalletBackendError(
4796
+ "approved preview payload does not match the approval token. Generate a new preview and approval before continue."
4797
+ )
4798
+ preview_summary = self._build_confirmation_summary(
4799
+ action_label="Solana private swap",
4800
+ payload=approved_preview,
4801
+ )
4802
+ summary_without_digest = {
4803
+ key: value
4804
+ for key, value in approval_summary_copy.items()
4805
+ if key != "_preview_digest"
4806
+ }
4807
+ if preview_summary != summary_without_digest:
4808
+ raise WalletBackendError(
4809
+ "approved preview payload does not match the approval token. Generate a new preview and approval before continue."
4810
+ )
4811
+
4812
+ self._require_execute_approval(
4813
+ approval_token=approval_token,
4814
+ tool_name="swap_solana_privately",
4815
+ summary=approval_summary_copy,
4816
+ action_label="Solana private swap",
4817
+ )
4818
+
4819
+ result = await self.backend.continue_solana_private_swap(
4820
+ approved_preview=approved_preview,
4821
+ existing_order=resume_private_swap_order,
4822
+ )
4823
+ return AgentToolResult(
4824
+ tool=tool_name,
4825
+ ok=True,
4826
+ data=self._annotate_sensitive_payload(
4827
+ result,
4828
+ action_label="Solana private swap funding",
4829
+ mode="execute",
4830
+ ),
4831
+ )
4832
+
4833
+ if tool_name == "get_solana_private_swap_status":
4834
+ multi_id = args.get("multi_id")
4835
+ houdini_id = args.get("houdini_id")
4836
+ if multi_id is not None and not isinstance(multi_id, str):
4837
+ raise WalletBackendError("multi_id must be a string when provided.")
4838
+ if houdini_id is not None and not isinstance(houdini_id, str):
4839
+ raise WalletBackendError("houdini_id must be a string when provided.")
4840
+ normalized_multi_id = multi_id.strip() if isinstance(multi_id, str) and multi_id.strip() else None
4841
+ normalized_houdini_id = (
4842
+ houdini_id.strip() if isinstance(houdini_id, str) and houdini_id.strip() else None
4843
+ )
4844
+ if normalized_multi_id is None and normalized_houdini_id is None:
4845
+ raise WalletBackendError("multi_id or houdini_id is required.")
4846
+ data = await self.backend.get_solana_private_swap_status(
4847
+ multi_id=normalized_multi_id,
4848
+ houdini_id=normalized_houdini_id,
4849
+ )
4850
+ return AgentToolResult(tool=tool_name, ok=True, data=data)
4851
+
4493
4852
  if tool_name == "swap_solana_lifi_cross_chain_tokens":
4494
4853
  input_token = args.get("input_token")
4495
4854
  destination_chain = args.get("destination_chain")
@@ -102,6 +102,12 @@ def _apply_config_overrides(config: dict[str, Any]) -> None:
102
102
  ),
103
103
  "jupiterLendBaseUrl": ("JUPITER_LEND_API_BASE_URL", config.get("jupiterLendBaseUrl"), True),
104
104
  "jupiterApiKey": ("JUPITER_API_KEY", config.get("jupiterApiKey"), True),
105
+ "houdiniBaseUrl": ("HOUDINI_API_BASE_URL", config.get("houdiniBaseUrl"), True),
106
+ "houdiniApiKey": ("HOUDINI_API_KEY", config.get("houdiniApiKey"), True),
107
+ "houdiniApiSecret": ("HOUDINI_API_SECRET", config.get("houdiniApiSecret"), True),
108
+ "houdiniUserIp": ("HOUDINI_USER_IP", config.get("houdiniUserIp"), True),
109
+ "houdiniUserAgent": ("HOUDINI_USER_AGENT", config.get("houdiniUserAgent"), True),
110
+ "houdiniUserTimezone": ("HOUDINI_USER_TIMEZONE", config.get("houdiniUserTimezone"), True),
105
111
  "kaminoBaseUrl": ("KAMINO_API_BASE_URL", config.get("kaminoBaseUrl"), True),
106
112
  "kaminoProgramId": ("KAMINO_PROGRAM_ID", config.get("kaminoProgramId"), True),
107
113
  }
@@ -615,7 +621,13 @@ def main() -> int:
615
621
  )
616
622
  )
617
623
  except Exception as exc:
618
- print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
624
+ error_payload: dict[str, Any] = {"ok": False, "error": str(exc)}
625
+ if isinstance(exc, WalletBackendError):
626
+ if exc.code:
627
+ error_payload["code"] = exc.code
628
+ if exc.details is not None:
629
+ error_payload["details"] = exc.details
630
+ print(json.dumps(error_payload), file=sys.stderr)
619
631
  return 1
620
632
 
621
633
  print(json.dumps(payload))