@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
@@ -11,7 +11,7 @@ from decimal import Decimal, InvalidOperation
11
11
  from typing import Any
12
12
 
13
13
  from agent_wallet.models import AgentWalletCapabilities, SolanaWalletState
14
- from agent_wallet.providers import bags, jupiter, kamino, lifi, solana_rpc
14
+ from agent_wallet.providers import bags, houdini, jupiter, kamino, lifi, solana_rpc
15
15
  from agent_wallet.solana_stake import (
16
16
  STAKE_STATE_V2_SIZE,
17
17
  deactivate_stake as build_deactivate_stake_instruction,
@@ -46,6 +46,7 @@ TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
46
46
  TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
47
47
  NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112"
48
48
  STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111"
49
+ HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS = 600
49
50
 
50
51
 
51
52
  def _load_signing_key():
@@ -699,6 +700,862 @@ class SolanaWalletBackend(AgentWalletBackend):
699
700
  "source": "lifi",
700
701
  }
701
702
 
703
+ @staticmethod
704
+ def _houdini_token_is_native(token: dict[str, Any]) -> bool:
705
+ chain = str(token.get("chain") or "").strip().lower()
706
+ symbol = str(token.get("symbol") or "").strip().lower()
707
+ address = str(token.get("address") or "").strip().lower()
708
+ return chain == "solana" and symbol == "sol" and address in {"", "sol", "native", "11111111111111111111111111111111", NATIVE_SOL_MINT.lower()}
709
+
710
+ def _normalize_houdini_private_preview_payload(
711
+ self,
712
+ *,
713
+ owner: str,
714
+ input_token: dict[str, Any],
715
+ output_token: dict[str, Any],
716
+ destination_address: str,
717
+ amount_ui: Decimal,
718
+ quote: dict[str, Any],
719
+ use_xmr: bool,
720
+ ) -> dict[str, Any]:
721
+ min_private = (
722
+ input_token.get("min_max_private")
723
+ if isinstance(input_token.get("min_max_private"), dict)
724
+ else {}
725
+ )
726
+ return {
727
+ "chain": "solana",
728
+ "network": self.network,
729
+ "mode": "preview",
730
+ "asset_type": "solana-private-swap",
731
+ "owner": owner,
732
+ "destination_address": destination_address,
733
+ "input_token_query": input_token.get("symbol"),
734
+ "output_token_query": output_token.get("symbol"),
735
+ "input_token_id": input_token.get("id"),
736
+ "output_token_id": output_token.get("id"),
737
+ "input_token_symbol": input_token.get("symbol"),
738
+ "output_token_symbol": output_token.get("symbol"),
739
+ "input_token_name": input_token.get("name"),
740
+ "output_token_name": output_token.get("name"),
741
+ "input_token_address": input_token.get("address"),
742
+ "output_token_address": output_token.get("address"),
743
+ "input_token_chain": input_token.get("chain"),
744
+ "output_token_chain": output_token.get("chain"),
745
+ "input_token_decimals": input_token.get("decimals"),
746
+ "output_token_decimals": output_token.get("decimals"),
747
+ "input_is_native": self._houdini_token_is_native(input_token),
748
+ "output_is_native": self._houdini_token_is_native(output_token),
749
+ "input_amount_ui": float(amount_ui),
750
+ "estimated_output_amount_ui": float(Decimal(str(quote.get("amountOut") or "0"))),
751
+ "estimated_output_amount_usd": quote.get("amountOutUsd"),
752
+ "input_private_min_ui": min_private.get("min"),
753
+ "input_private_max_ui": min_private.get("max"),
754
+ "private_duration_minutes": quote.get("duration"),
755
+ "quote_id": quote.get("quoteId"),
756
+ "quote_type": quote.get("type"),
757
+ "rewards_available": quote.get("rewardsAvailable"),
758
+ "anonymous": True,
759
+ "use_xmr": bool(use_xmr),
760
+ "can_send": self.get_capabilities().can_send_transaction,
761
+ "sign_only": self.sign_only,
762
+ "source": "houdini",
763
+ }
764
+
765
+ async def _wait_for_houdini_order_ready(
766
+ self,
767
+ *,
768
+ multi_id: str,
769
+ houdini_id: str,
770
+ timeout_seconds: float = 20.0,
771
+ poll_interval_seconds: float = 2.0,
772
+ ) -> dict[str, Any]:
773
+ deadline = asyncio.get_running_loop().time() + timeout_seconds
774
+ last_order: dict[str, Any] | None = None
775
+ while True:
776
+ status_payload = await houdini.fetch_multi_status(multi_id=multi_id)
777
+ orders = status_payload.get("orders")
778
+ if isinstance(orders, list):
779
+ for candidate in orders:
780
+ if (
781
+ isinstance(candidate, dict)
782
+ and str(candidate.get("houdiniId") or "").strip() == houdini_id
783
+ ):
784
+ last_order = candidate
785
+ break
786
+ if last_order is None:
787
+ raise WalletBackendError("Houdini order disappeared before funding could start.")
788
+ if str(last_order.get("statusLabel") or "").strip().upper() != "INITIALIZING":
789
+ return last_order
790
+ if asyncio.get_running_loop().time() >= deadline:
791
+ raise WalletBackendError(
792
+ "Houdini order stayed in INITIALIZING too long. Generate a new preview and try again."
793
+ )
794
+ await asyncio.sleep(poll_interval_seconds)
795
+
796
+ async def _wait_for_houdini_single_order_ready(
797
+ self,
798
+ *,
799
+ houdini_id: str,
800
+ initial_order: dict[str, Any] | None = None,
801
+ timeout_seconds: float = 180.0,
802
+ poll_interval_seconds: float = 5.0,
803
+ ) -> dict[str, Any]:
804
+ deadline = asyncio.get_running_loop().time() + timeout_seconds
805
+ last_order: dict[str, Any] | None = initial_order if isinstance(initial_order, dict) else None
806
+ while True:
807
+ order = await houdini.fetch_order_status(houdini_id=houdini_id)
808
+ if isinstance(order, dict) and order:
809
+ last_order = order
810
+ if str(order.get("statusLabel") or "").strip().upper() != "INITIALIZING":
811
+ return order
812
+ if asyncio.get_running_loop().time() >= deadline:
813
+ details = {
814
+ "houdini_id": houdini_id,
815
+ "multi_id": str((last_order or {}).get("multiId") or "").strip() or None,
816
+ "deposit_address": str((last_order or {}).get("depositAddress") or "").strip() or None,
817
+ "order_status": str((last_order or {}).get("statusLabel") or "").strip() or "INITIALIZING",
818
+ "order": last_order or order,
819
+ }
820
+ raise WalletBackendError(
821
+ "Houdini order stayed in INITIALIZING too long. Keep the created order and retry execute after the deposit account is ready.",
822
+ code="houdini_order_initializing_timeout",
823
+ details=details,
824
+ )
825
+ await asyncio.sleep(poll_interval_seconds)
826
+
827
+ async def _wait_for_houdini_spl_deposit_ready(
828
+ self,
829
+ *,
830
+ deposit_address: str,
831
+ mint: str,
832
+ timeout_seconds: float = 180.0,
833
+ poll_interval_seconds: float = 5.0,
834
+ ) -> dict[str, Any]:
835
+ deadline = asyncio.get_running_loop().time() + timeout_seconds
836
+ while True:
837
+ account_info = await solana_rpc.fetch_account_info(
838
+ deposit_address,
839
+ rpc_url=self.rpc_urls,
840
+ )
841
+ if account_info:
842
+ recipient_mint = (
843
+ (account_info or {})
844
+ .get("data", {})
845
+ .get("parsed", {})
846
+ .get("info", {})
847
+ .get("mint")
848
+ )
849
+ if recipient_mint and str(recipient_mint).strip() != mint:
850
+ raise WalletBackendError(
851
+ "Houdini deposit token account mint does not match the approved input token."
852
+ )
853
+ return {
854
+ "deposit_address": deposit_address,
855
+ "account_info": account_info,
856
+ "mint": recipient_mint,
857
+ }
858
+ if asyncio.get_running_loop().time() >= deadline:
859
+ raise WalletBackendError(
860
+ "Houdini order was created, but the Solana deposit token account was not ready in time. Generate a new preview and try again.",
861
+ code="houdini_deposit_not_ready",
862
+ details={"deposit_address": deposit_address, "mint": mint},
863
+ )
864
+ await asyncio.sleep(poll_interval_seconds)
865
+
866
+ async def preview_solana_private_swap(
867
+ self,
868
+ *,
869
+ input_token: str,
870
+ output_token: str,
871
+ destination_address: str,
872
+ amount_ui: float,
873
+ use_xmr: bool = False,
874
+ ) -> dict[str, Any]:
875
+ if self.network != "mainnet":
876
+ raise WalletBackendError("Houdini private Solana swaps are only enabled for Solana mainnet.")
877
+ owner = await self.get_address()
878
+ if not owner:
879
+ raise WalletBackendError(
880
+ "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
881
+ )
882
+ if not isinstance(destination_address, str) or not destination_address.strip():
883
+ raise WalletBackendError("destination_address is required.")
884
+ destination_address = validate_solana_address(destination_address.strip())
885
+ if not isinstance(amount_ui, (int, float)) or amount_ui <= 0:
886
+ raise WalletBackendError("amount must be a positive number.")
887
+
888
+ amount_decimal = Decimal(str(amount_ui))
889
+ resolved_input = await houdini.resolve_cex_token(term=input_token, chain="solana")
890
+ resolved_output = await houdini.resolve_cex_token(term=output_token, chain="solana")
891
+ if str(resolved_input.get("id") or "") != str(resolved_output.get("id") or ""):
892
+ raise WalletBackendError(
893
+ "The initial Houdini Solana private route supports same-token private payouts only. "
894
+ "Use the same token for input and output, for example SOL->SOL or USDC->USDC."
895
+ )
896
+
897
+ private_quotes = await houdini.fetch_private_quotes(
898
+ from_token_id=str(resolved_input["id"]),
899
+ to_token_id=str(resolved_output["id"]),
900
+ amount_ui=amount_decimal,
901
+ )
902
+ best_quote = houdini.select_best_private_quote(private_quotes)
903
+ return self._normalize_houdini_private_preview_payload(
904
+ owner=owner,
905
+ input_token=resolved_input,
906
+ output_token=resolved_output,
907
+ destination_address=destination_address,
908
+ amount_ui=amount_decimal,
909
+ quote=best_quote,
910
+ use_xmr=use_xmr,
911
+ )
912
+
913
+ async def get_solana_private_swap_status(
914
+ self,
915
+ *,
916
+ multi_id: str | None = None,
917
+ houdini_id: str | None = None,
918
+ ) -> dict[str, Any]:
919
+ if self.network != "mainnet":
920
+ raise WalletBackendError("Houdini private Solana swaps are only enabled for Solana mainnet.")
921
+ normalized_multi_id = str(multi_id or "").strip()
922
+ normalized_houdini_id = str(houdini_id or "").strip()
923
+ if not normalized_multi_id and not normalized_houdini_id:
924
+ raise WalletBackendError("multi_id or houdini_id is required.")
925
+
926
+ if not normalized_multi_id:
927
+ selected_order = await houdini.fetch_order_status(houdini_id=normalized_houdini_id)
928
+ selected_status = str(selected_order.get("statusLabel") or "").strip() or None
929
+ terminal_statuses = {"FINISHED", "FAILED", "EXPIRED", "REFUNDED"}
930
+ return {
931
+ "chain": "solana",
932
+ "network": self.network,
933
+ "asset_type": "solana-private-swap",
934
+ "multi_id": None,
935
+ "order_count": 1,
936
+ "orders": [selected_order],
937
+ "selected_order": selected_order,
938
+ "selected_houdini_id": (
939
+ str(selected_order.get("houdiniId") or "").strip() or normalized_houdini_id or None
940
+ ),
941
+ "selected_status": selected_status,
942
+ "all_terminal": bool(selected_status and selected_status.upper() in terminal_statuses),
943
+ "source": "houdini",
944
+ }
945
+
946
+ payload = await houdini.fetch_multi_status(multi_id=normalized_multi_id)
947
+ orders = payload.get("orders") if isinstance(payload.get("orders"), list) else []
948
+ selected_order = None
949
+ if normalized_houdini_id:
950
+ for order in orders:
951
+ if isinstance(order, dict) and str(order.get("houdiniId") or "").strip() == normalized_houdini_id:
952
+ selected_order = order
953
+ break
954
+ if selected_order is None:
955
+ raise WalletBackendError("houdini_id was not found inside the requested multi_id.")
956
+ elif len(orders) == 1 and isinstance(orders[0], dict):
957
+ selected_order = orders[0]
958
+
959
+ terminal_statuses = {"FINISHED", "FAILED", "EXPIRED", "REFUNDED"}
960
+ return {
961
+ "chain": "solana",
962
+ "network": self.network,
963
+ "asset_type": "solana-private-swap",
964
+ "multi_id": payload.get("multiId") or normalized_multi_id,
965
+ "order_count": len([item for item in orders if isinstance(item, dict)]),
966
+ "orders": orders,
967
+ "selected_order": selected_order,
968
+ "selected_houdini_id": (
969
+ str(selected_order.get("houdiniId") or "").strip()
970
+ if isinstance(selected_order, dict)
971
+ else None
972
+ ),
973
+ "selected_status": (
974
+ str(selected_order.get("statusLabel") or "").strip()
975
+ if isinstance(selected_order, dict)
976
+ else None
977
+ ),
978
+ "all_terminal": all(
979
+ isinstance(item, dict)
980
+ and str(item.get("statusLabel") or "").strip().upper() in terminal_statuses
981
+ for item in orders
982
+ )
983
+ if orders
984
+ else False,
985
+ "source": "houdini",
986
+ }
987
+
988
+ @staticmethod
989
+ def _pick_houdini_order_value(order: dict[str, Any], *keys: str) -> str:
990
+ for key in keys:
991
+ value = order.get(key)
992
+ if isinstance(value, str) and value.strip():
993
+ return value.strip()
994
+ if isinstance(value, (int, float, Decimal)):
995
+ text = str(value).strip()
996
+ if text:
997
+ return text
998
+ return ""
999
+
1000
+ @staticmethod
1001
+ def _normalize_houdini_token_address(value: str) -> str:
1002
+ normalized = str(value or "").strip()
1003
+ if not normalized:
1004
+ return ""
1005
+ lowered = normalized.lower()
1006
+ if lowered in {"sol", "native", "11111111111111111111111111111111"}:
1007
+ return NATIVE_SOL_MINT.lower()
1008
+ return lowered
1009
+
1010
+ def _validate_houdini_order_against_preview(
1011
+ self,
1012
+ *,
1013
+ order: dict[str, Any],
1014
+ preview: dict[str, Any],
1015
+ ) -> dict[str, Any]:
1016
+ if str(order.get("receiverAddress") or "").strip() != str(preview["destination_address"]):
1017
+ raise WalletBackendError("Houdini order receiverAddress does not match the approved destination.")
1018
+ if not bool(order.get("anonymous")):
1019
+ raise WalletBackendError("Houdini order is not marked anonymous as required.")
1020
+
1021
+ checks: dict[str, Any] = {
1022
+ "receiver_address": str(order.get("receiverAddress") or "").strip(),
1023
+ "anonymous": bool(order.get("anonymous")),
1024
+ }
1025
+ warnings: list[str] = []
1026
+
1027
+ expected_input_id = str(preview.get("input_token_id") or "").strip()
1028
+ order_input_id = self._pick_houdini_order_value(
1029
+ order,
1030
+ "from",
1031
+ "fromId",
1032
+ "inId",
1033
+ "inputTokenId",
1034
+ "fromTokenId",
1035
+ )
1036
+ if expected_input_id and order_input_id and order_input_id != expected_input_id:
1037
+ raise WalletBackendError("Houdini order input token id does not match the approved token.")
1038
+ checks["input_token_id"] = order_input_id or expected_input_id or None
1039
+
1040
+ expected_input_amount = Decimal(str(preview.get("input_amount_ui") or "0"))
1041
+ order_input_amount = Decimal(
1042
+ self._pick_houdini_order_value(order, "inAmount", "amountIn", "inputAmount") or "0"
1043
+ )
1044
+ if order_input_amount != expected_input_amount:
1045
+ raise WalletBackendError("Houdini order input amount does not match the approved preview.")
1046
+ checks["input_amount_ui"] = str(order_input_amount)
1047
+
1048
+ expected_output_id = str(preview.get("output_token_id") or "").strip()
1049
+ order_output_id = self._pick_houdini_order_value(
1050
+ order,
1051
+ "to",
1052
+ "toId",
1053
+ "outId",
1054
+ "outputTokenId",
1055
+ "toTokenId",
1056
+ )
1057
+ if expected_output_id and order_output_id and order_output_id != expected_output_id:
1058
+ raise WalletBackendError("Houdini order output token id does not match the approved token.")
1059
+ checks["output_token_id"] = order_output_id or expected_output_id or None
1060
+
1061
+ expected_input_address = self._normalize_houdini_token_address(
1062
+ str(preview.get("input_token_address") or "")
1063
+ )
1064
+ order_input_address = self._normalize_houdini_token_address(
1065
+ self._pick_houdini_order_value(
1066
+ order,
1067
+ "fromAddress",
1068
+ "inAddress",
1069
+ "inputTokenAddress",
1070
+ "fromTokenAddress",
1071
+ )
1072
+ )
1073
+ if expected_input_address and order_input_address and order_input_address != expected_input_address:
1074
+ raise WalletBackendError("Houdini order input token address does not match the approved token.")
1075
+ checks["input_token_address"] = order_input_address or expected_input_address or None
1076
+
1077
+ expected_output_address = self._normalize_houdini_token_address(
1078
+ str(preview.get("output_token_address") or "")
1079
+ )
1080
+ order_output_address = self._normalize_houdini_token_address(
1081
+ self._pick_houdini_order_value(
1082
+ order,
1083
+ "toAddress",
1084
+ "outAddress",
1085
+ "outputTokenAddress",
1086
+ "toTokenAddress",
1087
+ )
1088
+ )
1089
+ if expected_output_address and order_output_address and order_output_address != expected_output_address:
1090
+ raise WalletBackendError("Houdini order output token address does not match the approved token.")
1091
+ checks["output_token_address"] = order_output_address or expected_output_address or None
1092
+
1093
+ expected_input_symbol = str(preview.get("input_token_symbol") or "").strip().upper()
1094
+ order_input_symbol = self._pick_houdini_order_value(order, "inSymbol", "fromSymbol").upper()
1095
+ if expected_input_symbol and order_input_symbol and order_input_symbol != expected_input_symbol:
1096
+ warnings.append(
1097
+ "Houdini order input token symbol differs from the approved preview display symbol."
1098
+ )
1099
+ checks["input_token_symbol"] = order_input_symbol or expected_input_symbol or None
1100
+
1101
+ expected_output_symbol = str(preview.get("output_token_symbol") or "").strip().upper()
1102
+ order_output_symbol = self._pick_houdini_order_value(order, "outSymbol", "toSymbol").upper()
1103
+ if expected_output_symbol and order_output_symbol and order_output_symbol != expected_output_symbol:
1104
+ warnings.append(
1105
+ "Houdini order output token symbol differs from the approved preview display symbol."
1106
+ )
1107
+ checks["output_token_symbol"] = order_output_symbol or expected_output_symbol or None
1108
+
1109
+ return {
1110
+ "validated": True,
1111
+ "checks": checks,
1112
+ "warnings": warnings,
1113
+ }
1114
+
1115
+ def _validate_houdini_order_output_against_preview(
1116
+ self,
1117
+ *,
1118
+ order: dict[str, Any],
1119
+ preview: dict[str, Any],
1120
+ ) -> dict[str, Any]:
1121
+ expected_output = Decimal(str(preview["estimated_output_amount_ui"]))
1122
+ order_output = Decimal(
1123
+ self._pick_houdini_order_value(order, "outAmount", "amountOut", "outputAmount") or "0"
1124
+ )
1125
+ tolerance = (
1126
+ expected_output * Decimal(HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS) / Decimal(10_000)
1127
+ )
1128
+ minimum_allowed = expected_output - tolerance
1129
+ if order_output < minimum_allowed:
1130
+ raise WalletBackendError(
1131
+ "Houdini order output fell materially below the approved preview. Generate a new preview and approval before execute.",
1132
+ code="private_swap_quote_changed",
1133
+ details={
1134
+ "approved_estimated_output_amount_ui": str(expected_output),
1135
+ "order_output_amount_ui": str(order_output),
1136
+ "minimum_allowed_output_amount_ui": str(minimum_allowed),
1137
+ "allowed_drift_bps": HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS,
1138
+ },
1139
+ )
1140
+ warnings: list[str] = []
1141
+ if order_output < expected_output:
1142
+ warnings.append(
1143
+ "Houdini order output drifted slightly below the approved preview but stayed within the allowed tolerance."
1144
+ )
1145
+ return {
1146
+ "validated": True,
1147
+ "approved_estimated_output_amount_ui": str(expected_output),
1148
+ "order_output_amount_ui": str(order_output),
1149
+ "minimum_allowed_output_amount_ui": str(minimum_allowed),
1150
+ "allowed_drift_bps": HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS,
1151
+ "warnings": warnings,
1152
+ }
1153
+
1154
+ async def _send_houdini_exact_spl_deposit(
1155
+ self,
1156
+ *,
1157
+ recipient_token_account: str,
1158
+ mint: str,
1159
+ amount_raw: int,
1160
+ decimals: int,
1161
+ ) -> dict[str, Any]:
1162
+ if not self.signer:
1163
+ raise WalletBackendError("Solana signer is not configured.")
1164
+
1165
+ sender = await self.get_address()
1166
+ if not sender:
1167
+ raise WalletBackendError(
1168
+ "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
1169
+ )
1170
+
1171
+ recipient_token_account = validate_solana_address(recipient_token_account)
1172
+ mint = validate_solana_mint(mint)
1173
+ if amount_raw <= 0:
1174
+ raise WalletBackendError("Houdini SPL deposit amount must be greater than zero.")
1175
+
1176
+ try:
1177
+ from solders.hash import Hash
1178
+ from solders.keypair import Keypair
1179
+ from solders.message import Message
1180
+ from solders.pubkey import Pubkey
1181
+ from solders.transaction import Transaction
1182
+ from spl.token.instructions import (
1183
+ TransferCheckedParams,
1184
+ get_associated_token_address,
1185
+ transfer_checked,
1186
+ )
1187
+ except ImportError as exc:
1188
+ raise WalletBackendError(
1189
+ "solana and solders packages are required for SPL token transfers."
1190
+ ) from exc
1191
+
1192
+ sender_pubkey = Pubkey.from_string(sender)
1193
+ mint_pubkey = Pubkey.from_string(mint)
1194
+ token_program_id = await self._resolve_token_program_id(mint)
1195
+ token_program_pubkey = Pubkey.from_string(token_program_id)
1196
+ sender_token_account = str(
1197
+ get_associated_token_address(
1198
+ sender_pubkey,
1199
+ mint_pubkey,
1200
+ token_program_id=token_program_pubkey,
1201
+ )
1202
+ )
1203
+ sender_token_account_exists = await solana_rpc.account_exists(
1204
+ sender_token_account,
1205
+ rpc_url=self.rpc_urls,
1206
+ )
1207
+ if not sender_token_account_exists:
1208
+ raise WalletBackendError("Sender token account does not exist for this mint.")
1209
+ original_deposit_address = recipient_token_account
1210
+ await self._wait_for_houdini_spl_deposit_ready(
1211
+ deposit_address=recipient_token_account,
1212
+ mint=mint,
1213
+ )
1214
+
1215
+ sender_balance = await solana_rpc.fetch_token_account_balance(
1216
+ sender_token_account,
1217
+ rpc_url=self.rpc_urls,
1218
+ )
1219
+ sender_raw_balance = int(sender_balance.get("amount") or 0)
1220
+ if amount_raw > sender_raw_balance:
1221
+ raise WalletBackendError("Insufficient token balance for this private transfer.")
1222
+
1223
+ latest_blockhash = await solana_rpc.fetch_latest_blockhash(
1224
+ rpc_url=self.rpc_urls,
1225
+ commitment=self.commitment,
1226
+ )
1227
+ blockhash = Hash.from_string(str(latest_blockhash["blockhash"]))
1228
+ keypair = Keypair.from_bytes(self.signer.export_keypair_bytes())
1229
+ message = Message.new_with_blockhash(
1230
+ [
1231
+ transfer_checked(
1232
+ TransferCheckedParams(
1233
+ program_id=token_program_pubkey,
1234
+ source=Pubkey.from_string(sender_token_account),
1235
+ mint=mint_pubkey,
1236
+ dest=Pubkey.from_string(recipient_token_account),
1237
+ owner=sender_pubkey,
1238
+ amount=amount_raw,
1239
+ decimals=decimals,
1240
+ signers=[],
1241
+ )
1242
+ )
1243
+ ],
1244
+ sender_pubkey,
1245
+ blockhash,
1246
+ )
1247
+ transaction = Transaction([keypair], message, blockhash)
1248
+ submitted = await solana_rpc.send_transaction(
1249
+ transaction_base64=encode_transaction_base64(bytes(transaction)),
1250
+ rpc_url=self.rpc_urls,
1251
+ )
1252
+ signature = submitted.get("signature")
1253
+ status = None
1254
+ confirmed = False
1255
+ if isinstance(signature, str) and signature:
1256
+ status = await solana_rpc.wait_for_confirmation(
1257
+ signature=signature,
1258
+ rpc_url=self.rpc_urls,
1259
+ )
1260
+ confirmed = status is not None
1261
+
1262
+ return {
1263
+ "chain": "solana",
1264
+ "network": self.network,
1265
+ "mode": "execute",
1266
+ "asset_type": "spl",
1267
+ "from_address": sender,
1268
+ "to_address": recipient_token_account,
1269
+ "requested_deposit_address": original_deposit_address,
1270
+ "deposit_address_interpretation": "token_account",
1271
+ "mint": mint,
1272
+ "token_program_id": token_program_id,
1273
+ "sender_token_account": sender_token_account,
1274
+ "recipient_token_account": recipient_token_account,
1275
+ "recipient_token_account_exists_before": True,
1276
+ "recipient_token_account_created": False,
1277
+ "amount_ui": float(Decimal(amount_raw) / (Decimal(10) ** decimals)),
1278
+ "amount_raw": amount_raw,
1279
+ "decimals": decimals,
1280
+ "signature": signature,
1281
+ "broadcasted": bool(signature),
1282
+ "confirmed": confirmed,
1283
+ "confirmation_status": status.get("confirmationStatus") if status else None,
1284
+ "slot": status.get("slot") if status else None,
1285
+ "sign_only": self.sign_only,
1286
+ "source": "solana-rpc",
1287
+ "execute_response": submitted,
1288
+ }
1289
+
1290
+ async def execute_solana_private_swap(
1291
+ self,
1292
+ *,
1293
+ input_token: str,
1294
+ output_token: str,
1295
+ destination_address: str,
1296
+ amount_ui: float,
1297
+ use_xmr: bool = False,
1298
+ approved_preview: dict[str, Any] | None = None,
1299
+ existing_order: dict[str, Any] | None = None,
1300
+ ) -> dict[str, Any]:
1301
+ if self.network != "mainnet":
1302
+ raise WalletBackendError("Houdini private Solana swaps are only enabled for Solana mainnet.")
1303
+ if not self.signer:
1304
+ raise WalletBackendError("Solana signer is not configured.")
1305
+ if self.sign_only:
1306
+ raise WalletBackendError(
1307
+ "This wallet backend is in sign-only mode. Disable sign_only to broadcast transactions."
1308
+ )
1309
+
1310
+ preview = (
1311
+ dict(approved_preview)
1312
+ if isinstance(approved_preview, dict)
1313
+ else await self.preview_solana_private_swap(
1314
+ input_token=input_token,
1315
+ output_token=output_token,
1316
+ destination_address=destination_address,
1317
+ amount_ui=amount_ui,
1318
+ use_xmr=use_xmr,
1319
+ )
1320
+ )
1321
+ owner = str(preview.get("owner") or await self.get_address() or "").strip()
1322
+ if not owner:
1323
+ raise WalletBackendError("No Solana wallet address configured for Houdini execution.")
1324
+
1325
+ quote_id = str(preview.get("quote_id") or "").strip()
1326
+ if not quote_id:
1327
+ raise WalletBackendError("Approved private swap preview is missing quote_id.")
1328
+
1329
+ if isinstance(existing_order, dict) and existing_order:
1330
+ create_payload = dict(existing_order)
1331
+ order = (
1332
+ create_payload.get("order")
1333
+ if isinstance(create_payload.get("order"), dict)
1334
+ else create_payload
1335
+ )
1336
+ if not isinstance(order, dict):
1337
+ raise WalletBackendError("Stored Houdini private swap order is invalid.")
1338
+ multi_id = str(create_payload.get("multi_id") or create_payload.get("multiId") or order.get("multiId") or "").strip() or None
1339
+ houdini_id = str(
1340
+ create_payload.get("houdini_id")
1341
+ or create_payload.get("houdiniId")
1342
+ or order.get("houdiniId")
1343
+ or ""
1344
+ ).strip()
1345
+ if not houdini_id:
1346
+ raise WalletBackendError("Stored Houdini private swap order is missing houdini_id.")
1347
+ latest_order = await houdini.fetch_order_status(houdini_id=houdini_id)
1348
+ if isinstance(latest_order, dict) and latest_order:
1349
+ order = latest_order
1350
+
1351
+ order_validation = self._validate_houdini_order_against_preview(
1352
+ order=order,
1353
+ preview=preview,
1354
+ )
1355
+ output_validation = self._validate_houdini_order_output_against_preview(
1356
+ order=order,
1357
+ preview=preview,
1358
+ )
1359
+ input_decimals = int(preview.get("input_token_decimals") or (9 if bool(preview.get("input_is_native")) else 0))
1360
+ input_amount_raw = int(
1361
+ (Decimal(str(preview["input_amount_ui"])) * (Decimal(10) ** input_decimals))
1362
+ .to_integral_value()
1363
+ )
1364
+ deposit_address = str(order.get("depositAddress") or "").strip()
1365
+ if not deposit_address:
1366
+ raise WalletBackendError("Houdini private swap response is missing depositAddress.")
1367
+
1368
+ if bool(preview.get("input_is_native")):
1369
+ funding_result = await self.send_native_transfer(
1370
+ recipient=deposit_address,
1371
+ amount_native=float(Decimal(str(preview["input_amount_ui"]))),
1372
+ )
1373
+ else:
1374
+ try:
1375
+ funding_result = await self._send_houdini_exact_spl_deposit(
1376
+ recipient_token_account=deposit_address,
1377
+ mint=str(preview.get("input_token_address") or "").strip(),
1378
+ amount_raw=input_amount_raw,
1379
+ decimals=input_decimals,
1380
+ )
1381
+ except WalletBackendError as exc:
1382
+ if exc.code == "houdini_deposit_not_ready":
1383
+ details = dict(exc.details or {})
1384
+ details.update(
1385
+ {
1386
+ "multi_id": multi_id,
1387
+ "houdini_id": houdini_id,
1388
+ "deposit_address": deposit_address,
1389
+ "order_status": order.get("statusLabel"),
1390
+ "order": order,
1391
+ "input_token_address": str(preview.get("input_token_address") or "").strip(),
1392
+ "input_amount_raw": str(input_amount_raw),
1393
+ "input_decimals": input_decimals,
1394
+ }
1395
+ )
1396
+ raise WalletBackendError(
1397
+ "Houdini order exists, but its Solana deposit account is not ready yet. Retry continue for the existing order instead of generating a new preview.",
1398
+ code="houdini_deposit_not_ready",
1399
+ details=details,
1400
+ ) from exc
1401
+ raise
1402
+
1403
+ signature = str(funding_result.get("signature") or "").strip() or None
1404
+ confirmed = bool(funding_result.get("confirmed"))
1405
+
1406
+ return {
1407
+ "chain": "solana",
1408
+ "network": self.network,
1409
+ "mode": "execute",
1410
+ "asset_type": "solana-private-swap",
1411
+ "owner": owner,
1412
+ "destination_address": str(preview["destination_address"]),
1413
+ "input_token_id": preview["input_token_id"],
1414
+ "output_token_id": preview["output_token_id"],
1415
+ "input_token_symbol": preview["input_token_symbol"],
1416
+ "output_token_symbol": preview["output_token_symbol"],
1417
+ "input_token_address": preview["input_token_address"],
1418
+ "output_token_address": preview["output_token_address"],
1419
+ "input_is_native": bool(preview.get("input_is_native")),
1420
+ "input_amount_ui": preview["input_amount_ui"],
1421
+ "estimated_output_amount_ui": preview["estimated_output_amount_ui"],
1422
+ "private_duration_minutes": order.get("eta") or preview.get("private_duration_minutes"),
1423
+ "multi_id": multi_id,
1424
+ "houdini_id": houdini_id,
1425
+ "deposit_address": deposit_address,
1426
+ "order_status": order.get("statusLabel"),
1427
+ "order": order,
1428
+ "signature": signature,
1429
+ "broadcasted": bool(signature),
1430
+ "confirmed": confirmed,
1431
+ "confirmation_status": funding_result.get("confirmation_status"),
1432
+ "slot": funding_result.get("slot"),
1433
+ "verification": {
1434
+ "verified": True,
1435
+ "deposit_address": deposit_address,
1436
+ "amount_raw": str(input_amount_raw),
1437
+ "is_native_input": bool(preview.get("input_is_native")),
1438
+ "token_mint": (
1439
+ None
1440
+ if bool(preview.get("input_is_native"))
1441
+ else str(preview.get("input_token_address") or "").strip()
1442
+ ),
1443
+ "quote_bound_single_exchange": True,
1444
+ },
1445
+ "simulation": None,
1446
+ "provider_order_validation": order_validation,
1447
+ "output_validation": output_validation,
1448
+ "funding_transfer": funding_result,
1449
+ "execute_response": funding_result.get("execute_response"),
1450
+ "status_tracking": {
1451
+ "multi_id": multi_id,
1452
+ "houdini_id": houdini_id,
1453
+ "poll_status_tool": "get_solana_private_swap_status",
1454
+ },
1455
+ "source": "houdini",
1456
+ "execution_state": "funding_submitted",
1457
+ }
1458
+ else:
1459
+ try:
1460
+ create_payload = await houdini.create_exchange(
1461
+ quote_id=quote_id,
1462
+ destination_address=str(preview["destination_address"]),
1463
+ )
1464
+ except ProviderError as exc:
1465
+ details = dict(exc.details or {})
1466
+ gateway_error = details.get("error") if isinstance(details.get("error"), dict) else None
1467
+ if isinstance(gateway_error, dict) and str(gateway_error.get("code") or "").strip() == "RATE_LIMIT_EXCEEDED":
1468
+ retry_after = gateway_error.get("retryAfter")
1469
+ raise WalletBackendError(
1470
+ "Houdini exchange create is rate-limited right now. Wait and retry execute without generating a new preview.",
1471
+ code="houdini_exchange_rate_limited",
1472
+ details={
1473
+ "retry_after": retry_after,
1474
+ "quote_id": quote_id,
1475
+ "destination_address": str(preview["destination_address"]),
1476
+ "provider": getattr(exc, "provider", "houdini"),
1477
+ "upstream_error": gateway_error,
1478
+ },
1479
+ ) from exc
1480
+ raise
1481
+ order = create_payload.get("order") if isinstance(create_payload.get("order"), dict) else create_payload
1482
+ if not isinstance(order, dict):
1483
+ raise WalletBackendError("Houdini returned no order object for the private swap.")
1484
+ if isinstance(order.get("error"), dict):
1485
+ error = order["error"]
1486
+ raise WalletBackendError(
1487
+ f"Houdini rejected the private swap request: {error.get('message') or 'unknown error'}.",
1488
+ code=str(error.get("code") or "").strip() or None,
1489
+ details=error,
1490
+ )
1491
+
1492
+ multi_id = str(create_payload.get("multiId") or order.get("multiId") or "").strip() or None
1493
+ houdini_id = str(order.get("houdiniId") or create_payload.get("houdiniId") or "").strip()
1494
+ if not houdini_id:
1495
+ raise WalletBackendError("Houdini private swap response is missing the order identifier.")
1496
+ deposit_address = str(order.get("depositAddress") or "").strip()
1497
+ if not deposit_address:
1498
+ raise WalletBackendError("Houdini private swap response is missing depositAddress.")
1499
+
1500
+ order_validation = self._validate_houdini_order_against_preview(
1501
+ order=order,
1502
+ preview=preview,
1503
+ )
1504
+ output_validation = self._validate_houdini_order_output_against_preview(
1505
+ order=order,
1506
+ preview=preview,
1507
+ )
1508
+
1509
+ return {
1510
+ "chain": "solana",
1511
+ "network": self.network,
1512
+ "mode": "execute",
1513
+ "asset_type": "solana-private-swap",
1514
+ "owner": owner,
1515
+ "destination_address": str(preview["destination_address"]),
1516
+ "input_token_id": preview["input_token_id"],
1517
+ "output_token_id": preview["output_token_id"],
1518
+ "input_token_symbol": preview["input_token_symbol"],
1519
+ "output_token_symbol": preview["output_token_symbol"],
1520
+ "input_token_address": preview["input_token_address"],
1521
+ "output_token_address": preview["output_token_address"],
1522
+ "input_is_native": bool(preview.get("input_is_native")),
1523
+ "input_amount_ui": preview["input_amount_ui"],
1524
+ "estimated_output_amount_ui": preview["estimated_output_amount_ui"],
1525
+ "private_duration_minutes": order.get("eta") or preview.get("private_duration_minutes"),
1526
+ "multi_id": multi_id,
1527
+ "houdini_id": houdini_id,
1528
+ "deposit_address": deposit_address,
1529
+ "order_status": order.get("statusLabel"),
1530
+ "order": order,
1531
+ "provider_order_validation": order_validation,
1532
+ "output_validation": output_validation,
1533
+ "status_tracking": {
1534
+ "multi_id": multi_id,
1535
+ "houdini_id": houdini_id,
1536
+ "poll_status_tool": "get_solana_private_swap_status",
1537
+ },
1538
+ "source": "houdini",
1539
+ "execution_state": "awaiting_deposit_funding",
1540
+ "next_step": "Call continue_solana_private_swap with the same approved private swap context to submit the funding transfer.",
1541
+ }
1542
+
1543
+ async def continue_solana_private_swap(
1544
+ self,
1545
+ *,
1546
+ approved_preview: dict[str, Any],
1547
+ existing_order: dict[str, Any],
1548
+ ) -> dict[str, Any]:
1549
+ return await self.execute_solana_private_swap(
1550
+ input_token=str(approved_preview.get("input_token_query") or approved_preview.get("input_token_symbol") or ""),
1551
+ output_token=str(approved_preview.get("output_token_query") or approved_preview.get("output_token_symbol") or ""),
1552
+ destination_address=str(approved_preview.get("destination_address") or ""),
1553
+ amount_ui=float(approved_preview.get("input_amount_ui") or 0),
1554
+ use_xmr=bool(approved_preview.get("use_xmr", False)),
1555
+ approved_preview=approved_preview,
1556
+ existing_order=existing_order,
1557
+ )
1558
+
702
1559
  async def preview_solana_lifi_cross_chain_swap(
703
1560
  self,
704
1561
  *,
@@ -2250,28 +3107,40 @@ class SolanaWalletBackend(AgentWalletBackend):
2250
3107
  try:
2251
3108
  from solders.keypair import Keypair
2252
3109
  from solders.message import to_bytes_versioned
2253
- from solders.transaction import VersionedTransaction
3110
+ from solders.transaction import Transaction, VersionedTransaction
2254
3111
  except ImportError as exc:
2255
3112
  raise WalletBackendError(
2256
3113
  "solana and solders packages are required for provider transaction signing."
2257
3114
  ) from exc
2258
3115
 
2259
- unsigned_transaction = VersionedTransaction.from_bytes(
2260
- self._bags_decode_serialized_transaction_bytes(transaction_base64)
2261
- )
3116
+ raw_transaction = self._bags_decode_serialized_transaction_bytes(transaction_base64)
2262
3117
  keypair = Keypair.from_bytes(self.signer.export_keypair_bytes())
2263
- signature = keypair.sign_message(to_bytes_versioned(unsigned_transaction.message))
2264
- signatures = list(unsigned_transaction.signatures)
2265
- if wallet_signer_index >= len(signatures):
2266
- raise WalletBackendError(
2267
- "Provider transaction signer layout is incompatible with local signing."
3118
+ try:
3119
+ unsigned_transaction = VersionedTransaction.from_bytes(raw_transaction)
3120
+ signature = keypair.sign_message(to_bytes_versioned(unsigned_transaction.message))
3121
+ signatures = list(unsigned_transaction.signatures)
3122
+ if wallet_signer_index >= len(signatures):
3123
+ raise WalletBackendError(
3124
+ "Provider transaction signer layout is incompatible with local signing."
3125
+ )
3126
+ signatures[wallet_signer_index] = signature
3127
+ signed_transaction = VersionedTransaction.populate(
3128
+ unsigned_transaction.message,
3129
+ signatures,
2268
3130
  )
2269
- signatures[wallet_signer_index] = signature
2270
- signed_transaction = VersionedTransaction.populate(
2271
- unsigned_transaction.message,
2272
- signatures,
2273
- )
2274
- return encode_transaction_base64(bytes(signed_transaction))
3131
+ return encode_transaction_base64(bytes(signed_transaction))
3132
+ except Exception:
3133
+ unsigned_transaction = Transaction.from_bytes(raw_transaction)
3134
+ signatures = list(unsigned_transaction.signatures)
3135
+ if wallet_signer_index >= len(signatures):
3136
+ raise WalletBackendError(
3137
+ "Provider transaction signer layout is incompatible with local signing."
3138
+ )
3139
+ unsigned_transaction.partial_sign(
3140
+ [keypair],
3141
+ unsigned_transaction.message.recent_blockhash,
3142
+ )
3143
+ return encode_transaction_base64(bytes(unsigned_transaction))
2275
3144
 
2276
3145
  async def _prepare_jupiter_lend_transaction(
2277
3146
  self,