@agentlayer.tech/wallet 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/.openclaw/extensions/agent-wallet/index.ts +454 -18
  2. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +96 -0
  3. package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +2 -0
  4. package/CHANGELOG.md +25 -0
  5. package/README.md +43 -51
  6. package/agent-wallet/.env.example +11 -0
  7. package/agent-wallet/README.md +53 -0
  8. package/agent-wallet/agent_wallet/approval.py +4 -0
  9. package/agent-wallet/agent_wallet/config.py +6 -0
  10. package/agent-wallet/agent_wallet/exceptions.py +2 -1
  11. package/agent-wallet/agent_wallet/openclaw_adapter.py +361 -2
  12. package/agent-wallet/agent_wallet/openclaw_cli.py +13 -1
  13. package/agent-wallet/agent_wallet/openclaw_runtime.py +2 -5
  14. package/agent-wallet/agent_wallet/providers/houdini.py +539 -0
  15. package/agent-wallet/agent_wallet/transaction_policy.py +251 -0
  16. package/agent-wallet/agent_wallet/user_wallets.py +83 -0
  17. package/agent-wallet/agent_wallet/wallet_layer/base.py +40 -0
  18. package/agent-wallet/agent_wallet/wallet_layer/solana.py +885 -16
  19. package/agent-wallet/pyproject.toml +1 -1
  20. package/agent-wallet/scripts/bootstrap_openclaw_evm.py +291 -0
  21. package/agent-wallet/scripts/install_agent_wallet.py +54 -2
  22. package/agent-wallet/scripts/install_openclaw_local_config.py +84 -4
  23. package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +343 -0
  24. package/agent-wallet/scripts/setup_evm_wallet.sh +151 -0
  25. package/hermes/plugins/agent_wallet/__init__.py +28 -2
  26. package/hermes/plugins/agent_wallet/plugin.yaml +2 -0
  27. package/hermes/plugins/agent_wallet/schemas.py +72 -0
  28. package/hermes/plugins/agent_wallet/tools.py +193 -9
  29. package/package.json +2 -2
@@ -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))
@@ -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 create_wallet_backend_for_user, ensure_user_solana_wallet, resolve_user_wallet_path
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
- wallet_path = resolve_user_wallet_path(user_id, network=network)
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,