@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.
- 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/README.md +15 -1
- 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 +75 -4
- package/hermes/plugins/agent_wallet/tools.py +93 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|

|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
[](https://www.npmjs.com/package/@agentlayer.tech/wallet)
|
|
5
|
+
[](https://www.npmjs.com/package/@agentlayer.tech/wallet)
|
|
6
|
+
[](https://github.com/lopushok9/Agent-Layer/blob/main/LICENSE)
|
|
3
7
|
|
|
4
8
|
```bash
|
|
5
9
|
npx @agentlayer.tech/wallet install --yes
|
|
@@ -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
|
package/agent-wallet/README.md
CHANGED
|
@@ -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":
|
|
648
|
-
"execute_requires_mainnet_confirmed_in_token":
|
|
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
|
-
|
|
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))
|
|
@@ -14,7 +14,7 @@ from agent_wallet.openclaw_adapter import OpenClawWalletAdapter
|
|
|
14
14
|
from agent_wallet.plugin_bundle import build_openclaw_plugin_bundle
|
|
15
15
|
from agent_wallet.providers.wdk_btc_local import WdkBtcLocalClient
|
|
16
16
|
from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient
|
|
17
|
-
from agent_wallet.user_wallets import
|
|
17
|
+
from agent_wallet.user_wallets import create_openclaw_solana_backend
|
|
18
18
|
from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendError
|
|
19
19
|
from agent_wallet.wallet_layer.wdk_evm import WdkEvmLocalWalletBackend
|
|
20
20
|
from agent_wallet.wallet_layer.wdk_btc import WdkBtcLocalWalletBackend
|
|
@@ -251,10 +251,7 @@ def onboard_openclaw_user_wallet(
|
|
|
251
251
|
plugin_bundle=plugin_bundle,
|
|
252
252
|
)
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
created_now = not wallet_path.exists()
|
|
256
|
-
wallet_info = ensure_user_solana_wallet(user_id, network=network)
|
|
257
|
-
backend = create_wallet_backend_for_user(
|
|
254
|
+
backend, wallet_info, created_now = create_openclaw_solana_backend(
|
|
258
255
|
user_id,
|
|
259
256
|
sign_only=sign_only,
|
|
260
257
|
network=network,
|