@agentlayer.tech/wallet 0.1.30 → 0.1.32

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 (39) hide show
  1. package/.openclaw/extensions/agent-wallet/README.md +1 -2
  2. package/.openclaw/extensions/agent-wallet/dist/index.js +6 -340
  3. package/.openclaw/extensions/agent-wallet/index.ts +6 -340
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +0 -43
  5. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  6. package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +1 -3
  7. package/CHANGELOG.md +35 -0
  8. package/README.md +0 -5
  9. package/agent-wallet/.env.example +0 -12
  10. package/agent-wallet/README.md +0 -35
  11. package/agent-wallet/agent_wallet/btc_user_wallets.py +32 -1
  12. package/agent-wallet/agent_wallet/config.py +11 -7
  13. package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -0
  14. package/agent-wallet/agent_wallet/openclaw_adapter.py +1 -655
  15. package/agent-wallet/agent_wallet/openclaw_cli.py +0 -7
  16. package/agent-wallet/agent_wallet/providers/evm_portfolio.py +18 -42
  17. package/agent-wallet/agent_wallet/providers/jupiter.py +1 -307
  18. package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +31 -3
  19. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +37 -3
  20. package/agent-wallet/agent_wallet/transaction_policy.py +0 -262
  21. package/agent-wallet/agent_wallet/wallet_layer/base.py +0 -100
  22. package/agent-wallet/agent_wallet/wallet_layer/solana.py +1 -1118
  23. package/agent-wallet/openclaw.plugin.json +0 -4
  24. package/agent-wallet/pyproject.toml +1 -1
  25. package/agent-wallet/scripts/install_agent_wallet.py +113 -6
  26. package/agent-wallet/scripts/install_openclaw_local_config.py +7 -5
  27. package/agent-wallet/skills/wallet-operator/SKILL.md +1 -5
  28. package/bin/openclaw-agent-wallet.mjs +103 -36
  29. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +7 -2
  30. package/codex/plugins/agent-wallet/server.py +2 -118
  31. package/hermes/plugins/agent_wallet/tools.py +1 -1
  32. package/package.json +1 -1
  33. package/wdk-btc-wallet/src/local_vault.js +45 -68
  34. package/wdk-btc-wallet/src/server.js +1 -0
  35. package/wdk-evm-wallet/README.md +4 -3
  36. package/wdk-evm-wallet/src/config.js +15 -0
  37. package/wdk-evm-wallet/src/local_vault.js +45 -68
  38. package/wdk-evm-wallet/src/server.js +1 -0
  39. package/agent-wallet/agent_wallet/providers/houdini.py +0 -539
@@ -13,7 +13,7 @@ from typing import Any
13
13
 
14
14
  from agent_wallet.config import normalize_solana_network
15
15
  from agent_wallet.models import AgentWalletCapabilities, SolanaWalletState
16
- from agent_wallet.providers import bags, flash, flash_sdk_bridge, houdini, jupiter, kamino, lifi, solana_rpc
16
+ from agent_wallet.providers import bags, flash, flash_sdk_bridge, jupiter, kamino, lifi, solana_rpc
17
17
  from agent_wallet.solana_stake import (
18
18
  STAKE_STATE_V2_SIZE,
19
19
  deactivate_stake as build_deactivate_stake_instruction,
@@ -30,7 +30,6 @@ from agent_wallet.transaction_policy import (
30
30
  verify_provider_bags_transaction,
31
31
  verify_provider_flash_transaction,
32
32
  verify_provider_kamino_lend_transaction,
33
- verify_provider_lend_transaction,
34
33
  verify_provider_swap_simulation_result,
35
34
  verify_provider_swap_transaction,
36
35
  )
@@ -49,7 +48,6 @@ TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
49
48
  TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
50
49
  NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112"
51
50
  STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111"
52
- HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS = 600
53
51
  SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS = 300
54
52
  SOLANA_SWAP_INTENT_DEFAULT_MAX_FEE_LAMPORTS = 6_000_000
55
53
  KAMINO_OPEN_POSITIONS_SCAN_CONCURRENCY = 6
@@ -722,861 +720,6 @@ class SolanaWalletBackend(AgentWalletBackend):
722
720
  }
723
721
 
724
722
  @staticmethod
725
- def _houdini_token_is_native(token: dict[str, Any]) -> bool:
726
- chain = str(token.get("chain") or "").strip().lower()
727
- symbol = str(token.get("symbol") or "").strip().lower()
728
- address = str(token.get("address") or "").strip().lower()
729
- return chain == "solana" and symbol == "sol" and address in {"", "sol", "native", "11111111111111111111111111111111", NATIVE_SOL_MINT.lower()}
730
-
731
- def _normalize_houdini_private_preview_payload(
732
- self,
733
- *,
734
- owner: str,
735
- input_token: dict[str, Any],
736
- output_token: dict[str, Any],
737
- destination_address: str,
738
- amount_ui: Decimal,
739
- quote: dict[str, Any],
740
- use_xmr: bool,
741
- ) -> dict[str, Any]:
742
- min_private = (
743
- input_token.get("min_max_private")
744
- if isinstance(input_token.get("min_max_private"), dict)
745
- else {}
746
- )
747
- return {
748
- "chain": "solana",
749
- "network": self.network,
750
- "mode": "preview",
751
- "asset_type": "solana-private-swap",
752
- "owner": owner,
753
- "destination_address": destination_address,
754
- "input_token_query": input_token.get("symbol"),
755
- "output_token_query": output_token.get("symbol"),
756
- "input_token_id": input_token.get("id"),
757
- "output_token_id": output_token.get("id"),
758
- "input_token_symbol": input_token.get("symbol"),
759
- "output_token_symbol": output_token.get("symbol"),
760
- "input_token_name": input_token.get("name"),
761
- "output_token_name": output_token.get("name"),
762
- "input_token_address": input_token.get("address"),
763
- "output_token_address": output_token.get("address"),
764
- "input_token_chain": input_token.get("chain"),
765
- "output_token_chain": output_token.get("chain"),
766
- "input_token_decimals": input_token.get("decimals"),
767
- "output_token_decimals": output_token.get("decimals"),
768
- "input_is_native": self._houdini_token_is_native(input_token),
769
- "output_is_native": self._houdini_token_is_native(output_token),
770
- "input_amount_ui": float(amount_ui),
771
- "estimated_output_amount_ui": float(Decimal(str(quote.get("amountOut") or "0"))),
772
- "estimated_output_amount_usd": quote.get("amountOutUsd"),
773
- "input_private_min_ui": min_private.get("min"),
774
- "input_private_max_ui": min_private.get("max"),
775
- "private_duration_minutes": quote.get("duration"),
776
- "quote_id": quote.get("quoteId"),
777
- "quote_type": quote.get("type"),
778
- "rewards_available": quote.get("rewardsAvailable"),
779
- "anonymous": True,
780
- "use_xmr": bool(use_xmr),
781
- "can_send": self.get_capabilities().can_send_transaction,
782
- "sign_only": self.sign_only,
783
- "source": "houdini",
784
- }
785
-
786
- async def _wait_for_houdini_order_ready(
787
- self,
788
- *,
789
- multi_id: str,
790
- houdini_id: str,
791
- timeout_seconds: float = 20.0,
792
- poll_interval_seconds: float = 2.0,
793
- ) -> dict[str, Any]:
794
- deadline = asyncio.get_running_loop().time() + timeout_seconds
795
- last_order: dict[str, Any] | None = None
796
- while True:
797
- status_payload = await houdini.fetch_multi_status(multi_id=multi_id)
798
- orders = status_payload.get("orders")
799
- if isinstance(orders, list):
800
- for candidate in orders:
801
- if (
802
- isinstance(candidate, dict)
803
- and str(candidate.get("houdiniId") or "").strip() == houdini_id
804
- ):
805
- last_order = candidate
806
- break
807
- if last_order is None:
808
- raise WalletBackendError("Houdini order disappeared before funding could start.")
809
- if str(last_order.get("statusLabel") or "").strip().upper() != "INITIALIZING":
810
- return last_order
811
- if asyncio.get_running_loop().time() >= deadline:
812
- raise WalletBackendError(
813
- "Houdini order stayed in INITIALIZING too long. Generate a new preview and try again."
814
- )
815
- await asyncio.sleep(poll_interval_seconds)
816
-
817
- async def _wait_for_houdini_single_order_ready(
818
- self,
819
- *,
820
- houdini_id: str,
821
- initial_order: dict[str, Any] | None = None,
822
- timeout_seconds: float = 180.0,
823
- poll_interval_seconds: float = 5.0,
824
- ) -> dict[str, Any]:
825
- deadline = asyncio.get_running_loop().time() + timeout_seconds
826
- last_order: dict[str, Any] | None = initial_order if isinstance(initial_order, dict) else None
827
- while True:
828
- order = await houdini.fetch_order_status(houdini_id=houdini_id)
829
- if isinstance(order, dict) and order:
830
- last_order = order
831
- if str(order.get("statusLabel") or "").strip().upper() != "INITIALIZING":
832
- return order
833
- if asyncio.get_running_loop().time() >= deadline:
834
- details = {
835
- "houdini_id": houdini_id,
836
- "multi_id": str((last_order or {}).get("multiId") or "").strip() or None,
837
- "deposit_address": str((last_order or {}).get("depositAddress") or "").strip() or None,
838
- "order_status": str((last_order or {}).get("statusLabel") or "").strip() or "INITIALIZING",
839
- "order": last_order or order,
840
- }
841
- raise WalletBackendError(
842
- "Houdini order stayed in INITIALIZING too long. Keep the created order and retry execute after the deposit account is ready.",
843
- code="houdini_order_initializing_timeout",
844
- details=details,
845
- )
846
- await asyncio.sleep(poll_interval_seconds)
847
-
848
- async def _wait_for_houdini_spl_deposit_ready(
849
- self,
850
- *,
851
- deposit_address: str,
852
- mint: str,
853
- timeout_seconds: float = 180.0,
854
- poll_interval_seconds: float = 5.0,
855
- ) -> dict[str, Any]:
856
- deadline = asyncio.get_running_loop().time() + timeout_seconds
857
- while True:
858
- account_info = await solana_rpc.fetch_account_info(
859
- deposit_address,
860
- rpc_url=self.rpc_urls,
861
- )
862
- if account_info:
863
- recipient_mint = (
864
- (account_info or {})
865
- .get("data", {})
866
- .get("parsed", {})
867
- .get("info", {})
868
- .get("mint")
869
- )
870
- if recipient_mint and str(recipient_mint).strip() != mint:
871
- raise WalletBackendError(
872
- "Houdini deposit token account mint does not match the approved input token."
873
- )
874
- return {
875
- "deposit_address": deposit_address,
876
- "account_info": account_info,
877
- "mint": recipient_mint,
878
- }
879
- if asyncio.get_running_loop().time() >= deadline:
880
- raise WalletBackendError(
881
- "Houdini order was created, but the Solana deposit token account was not ready in time. Generate a new preview and try again.",
882
- code="houdini_deposit_not_ready",
883
- details={"deposit_address": deposit_address, "mint": mint},
884
- )
885
- await asyncio.sleep(poll_interval_seconds)
886
-
887
- async def preview_solana_private_swap(
888
- self,
889
- *,
890
- input_token: str,
891
- output_token: str,
892
- destination_address: str,
893
- amount_ui: float,
894
- use_xmr: bool = False,
895
- ) -> dict[str, Any]:
896
- if self.network != "mainnet":
897
- raise WalletBackendError("Houdini private Solana swaps are only enabled for Solana mainnet.")
898
- owner = await self.get_address()
899
- if not owner:
900
- raise WalletBackendError(
901
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
902
- )
903
- if not isinstance(destination_address, str) or not destination_address.strip():
904
- raise WalletBackendError("destination_address is required.")
905
- destination_address = validate_solana_address(destination_address.strip())
906
- if not isinstance(amount_ui, (int, float)) or amount_ui <= 0:
907
- raise WalletBackendError("amount must be a positive number.")
908
-
909
- amount_decimal = Decimal(str(amount_ui))
910
- resolved_input = await houdini.resolve_cex_token(term=input_token, chain="solana")
911
- resolved_output = await houdini.resolve_cex_token(term=output_token, chain="solana")
912
- if str(resolved_input.get("id") or "") != str(resolved_output.get("id") or ""):
913
- raise WalletBackendError(
914
- "The initial Houdini Solana private route supports same-token private payouts only. "
915
- "Use the same token for input and output, for example SOL->SOL or USDC->USDC."
916
- )
917
-
918
- private_quotes = await houdini.fetch_private_quotes(
919
- from_token_id=str(resolved_input["id"]),
920
- to_token_id=str(resolved_output["id"]),
921
- amount_ui=amount_decimal,
922
- )
923
- best_quote = houdini.select_best_private_quote(private_quotes)
924
- return self._normalize_houdini_private_preview_payload(
925
- owner=owner,
926
- input_token=resolved_input,
927
- output_token=resolved_output,
928
- destination_address=destination_address,
929
- amount_ui=amount_decimal,
930
- quote=best_quote,
931
- use_xmr=use_xmr,
932
- )
933
-
934
- async def get_solana_private_swap_status(
935
- self,
936
- *,
937
- multi_id: str | None = None,
938
- houdini_id: str | None = None,
939
- ) -> dict[str, Any]:
940
- if self.network != "mainnet":
941
- raise WalletBackendError("Houdini private Solana swaps are only enabled for Solana mainnet.")
942
- normalized_multi_id = str(multi_id or "").strip()
943
- normalized_houdini_id = str(houdini_id or "").strip()
944
- if not normalized_multi_id and not normalized_houdini_id:
945
- raise WalletBackendError("multi_id or houdini_id is required.")
946
-
947
- if not normalized_multi_id:
948
- selected_order = await houdini.fetch_order_status(houdini_id=normalized_houdini_id)
949
- selected_status = str(selected_order.get("statusLabel") or "").strip() or None
950
- terminal_statuses = {"FINISHED", "FAILED", "EXPIRED", "REFUNDED"}
951
- return {
952
- "chain": "solana",
953
- "network": self.network,
954
- "asset_type": "solana-private-swap",
955
- "multi_id": None,
956
- "order_count": 1,
957
- "orders": [selected_order],
958
- "selected_order": selected_order,
959
- "selected_houdini_id": (
960
- str(selected_order.get("houdiniId") or "").strip() or normalized_houdini_id or None
961
- ),
962
- "selected_status": selected_status,
963
- "all_terminal": bool(selected_status and selected_status.upper() in terminal_statuses),
964
- "source": "houdini",
965
- }
966
-
967
- payload = await houdini.fetch_multi_status(multi_id=normalized_multi_id)
968
- orders = payload.get("orders") if isinstance(payload.get("orders"), list) else []
969
- selected_order = None
970
- if normalized_houdini_id:
971
- for order in orders:
972
- if isinstance(order, dict) and str(order.get("houdiniId") or "").strip() == normalized_houdini_id:
973
- selected_order = order
974
- break
975
- if selected_order is None:
976
- raise WalletBackendError("houdini_id was not found inside the requested multi_id.")
977
- elif len(orders) == 1 and isinstance(orders[0], dict):
978
- selected_order = orders[0]
979
-
980
- terminal_statuses = {"FINISHED", "FAILED", "EXPIRED", "REFUNDED"}
981
- return {
982
- "chain": "solana",
983
- "network": self.network,
984
- "asset_type": "solana-private-swap",
985
- "multi_id": payload.get("multiId") or normalized_multi_id,
986
- "order_count": len([item for item in orders if isinstance(item, dict)]),
987
- "orders": orders,
988
- "selected_order": selected_order,
989
- "selected_houdini_id": (
990
- str(selected_order.get("houdiniId") or "").strip()
991
- if isinstance(selected_order, dict)
992
- else None
993
- ),
994
- "selected_status": (
995
- str(selected_order.get("statusLabel") or "").strip()
996
- if isinstance(selected_order, dict)
997
- else None
998
- ),
999
- "all_terminal": all(
1000
- isinstance(item, dict)
1001
- and str(item.get("statusLabel") or "").strip().upper() in terminal_statuses
1002
- for item in orders
1003
- )
1004
- if orders
1005
- else False,
1006
- "source": "houdini",
1007
- }
1008
-
1009
- @staticmethod
1010
- def _pick_houdini_order_value(order: dict[str, Any], *keys: str) -> str:
1011
- for key in keys:
1012
- value = order.get(key)
1013
- if isinstance(value, str) and value.strip():
1014
- return value.strip()
1015
- if isinstance(value, (int, float, Decimal)):
1016
- text = str(value).strip()
1017
- if text:
1018
- return text
1019
- return ""
1020
-
1021
- @staticmethod
1022
- def _normalize_houdini_token_address(value: str) -> str:
1023
- normalized = str(value or "").strip()
1024
- if not normalized:
1025
- return ""
1026
- lowered = normalized.lower()
1027
- if lowered in {"sol", "native", "11111111111111111111111111111111"}:
1028
- return NATIVE_SOL_MINT.lower()
1029
- return lowered
1030
-
1031
- def _validate_houdini_order_against_preview(
1032
- self,
1033
- *,
1034
- order: dict[str, Any],
1035
- preview: dict[str, Any],
1036
- ) -> dict[str, Any]:
1037
- if str(order.get("receiverAddress") or "").strip() != str(preview["destination_address"]):
1038
- raise WalletBackendError("Houdini order receiverAddress does not match the approved destination.")
1039
- if not bool(order.get("anonymous")):
1040
- raise WalletBackendError("Houdini order is not marked anonymous as required.")
1041
-
1042
- checks: dict[str, Any] = {
1043
- "receiver_address": str(order.get("receiverAddress") or "").strip(),
1044
- "anonymous": bool(order.get("anonymous")),
1045
- }
1046
- warnings: list[str] = []
1047
-
1048
- expected_input_id = str(preview.get("input_token_id") or "").strip()
1049
- order_input_id = self._pick_houdini_order_value(
1050
- order,
1051
- "from",
1052
- "fromId",
1053
- "inId",
1054
- "inputTokenId",
1055
- "fromTokenId",
1056
- )
1057
- if expected_input_id and order_input_id and order_input_id != expected_input_id:
1058
- raise WalletBackendError("Houdini order input token id does not match the approved token.")
1059
- checks["input_token_id"] = order_input_id or expected_input_id or None
1060
-
1061
- expected_input_amount = Decimal(str(preview.get("input_amount_ui") or "0"))
1062
- order_input_amount = Decimal(
1063
- self._pick_houdini_order_value(order, "inAmount", "amountIn", "inputAmount") or "0"
1064
- )
1065
- if order_input_amount != expected_input_amount:
1066
- raise WalletBackendError("Houdini order input amount does not match the approved preview.")
1067
- checks["input_amount_ui"] = str(order_input_amount)
1068
-
1069
- expected_output_id = str(preview.get("output_token_id") or "").strip()
1070
- order_output_id = self._pick_houdini_order_value(
1071
- order,
1072
- "to",
1073
- "toId",
1074
- "outId",
1075
- "outputTokenId",
1076
- "toTokenId",
1077
- )
1078
- if expected_output_id and order_output_id and order_output_id != expected_output_id:
1079
- raise WalletBackendError("Houdini order output token id does not match the approved token.")
1080
- checks["output_token_id"] = order_output_id or expected_output_id or None
1081
-
1082
- expected_input_address = self._normalize_houdini_token_address(
1083
- str(preview.get("input_token_address") or "")
1084
- )
1085
- order_input_address = self._normalize_houdini_token_address(
1086
- self._pick_houdini_order_value(
1087
- order,
1088
- "fromAddress",
1089
- "inAddress",
1090
- "inputTokenAddress",
1091
- "fromTokenAddress",
1092
- )
1093
- )
1094
- if expected_input_address and order_input_address and order_input_address != expected_input_address:
1095
- raise WalletBackendError("Houdini order input token address does not match the approved token.")
1096
- checks["input_token_address"] = order_input_address or expected_input_address or None
1097
-
1098
- expected_output_address = self._normalize_houdini_token_address(
1099
- str(preview.get("output_token_address") or "")
1100
- )
1101
- order_output_address = self._normalize_houdini_token_address(
1102
- self._pick_houdini_order_value(
1103
- order,
1104
- "toAddress",
1105
- "outAddress",
1106
- "outputTokenAddress",
1107
- "toTokenAddress",
1108
- )
1109
- )
1110
- if expected_output_address and order_output_address and order_output_address != expected_output_address:
1111
- raise WalletBackendError("Houdini order output token address does not match the approved token.")
1112
- checks["output_token_address"] = order_output_address or expected_output_address or None
1113
-
1114
- expected_input_symbol = str(preview.get("input_token_symbol") or "").strip().upper()
1115
- order_input_symbol = self._pick_houdini_order_value(order, "inSymbol", "fromSymbol").upper()
1116
- if expected_input_symbol and order_input_symbol and order_input_symbol != expected_input_symbol:
1117
- warnings.append(
1118
- "Houdini order input token symbol differs from the approved preview display symbol."
1119
- )
1120
- checks["input_token_symbol"] = order_input_symbol or expected_input_symbol or None
1121
-
1122
- expected_output_symbol = str(preview.get("output_token_symbol") or "").strip().upper()
1123
- order_output_symbol = self._pick_houdini_order_value(order, "outSymbol", "toSymbol").upper()
1124
- if expected_output_symbol and order_output_symbol and order_output_symbol != expected_output_symbol:
1125
- warnings.append(
1126
- "Houdini order output token symbol differs from the approved preview display symbol."
1127
- )
1128
- checks["output_token_symbol"] = order_output_symbol or expected_output_symbol or None
1129
-
1130
- return {
1131
- "validated": True,
1132
- "checks": checks,
1133
- "warnings": warnings,
1134
- }
1135
-
1136
- def _validate_houdini_order_output_against_preview(
1137
- self,
1138
- *,
1139
- order: dict[str, Any],
1140
- preview: dict[str, Any],
1141
- ) -> dict[str, Any]:
1142
- expected_output = Decimal(str(preview["estimated_output_amount_ui"]))
1143
- order_output = Decimal(
1144
- self._pick_houdini_order_value(order, "outAmount", "amountOut", "outputAmount") or "0"
1145
- )
1146
- tolerance = (
1147
- expected_output * Decimal(HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS) / Decimal(10_000)
1148
- )
1149
- minimum_allowed = expected_output - tolerance
1150
- if order_output < minimum_allowed:
1151
- raise WalletBackendError(
1152
- "Houdini order output fell materially below the approved preview. Generate a new preview and approval before execute.",
1153
- code="private_swap_quote_changed",
1154
- details={
1155
- "approved_estimated_output_amount_ui": str(expected_output),
1156
- "order_output_amount_ui": str(order_output),
1157
- "minimum_allowed_output_amount_ui": str(minimum_allowed),
1158
- "allowed_drift_bps": HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS,
1159
- },
1160
- )
1161
- warnings: list[str] = []
1162
- if order_output < expected_output:
1163
- warnings.append(
1164
- "Houdini order output drifted slightly below the approved preview but stayed within the allowed tolerance."
1165
- )
1166
- return {
1167
- "validated": True,
1168
- "approved_estimated_output_amount_ui": str(expected_output),
1169
- "order_output_amount_ui": str(order_output),
1170
- "minimum_allowed_output_amount_ui": str(minimum_allowed),
1171
- "allowed_drift_bps": HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS,
1172
- "warnings": warnings,
1173
- }
1174
-
1175
- async def _send_houdini_exact_spl_deposit(
1176
- self,
1177
- *,
1178
- recipient_token_account: str,
1179
- mint: str,
1180
- amount_raw: int,
1181
- decimals: int,
1182
- ) -> dict[str, Any]:
1183
- if not self.signer:
1184
- raise WalletBackendError("Solana signer is not configured.")
1185
-
1186
- sender = await self.get_address()
1187
- if not sender:
1188
- raise WalletBackendError(
1189
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
1190
- )
1191
-
1192
- recipient_token_account = validate_solana_address(recipient_token_account)
1193
- mint = validate_solana_mint(mint)
1194
- if amount_raw <= 0:
1195
- raise WalletBackendError("Houdini SPL deposit amount must be greater than zero.")
1196
-
1197
- try:
1198
- from solders.hash import Hash
1199
- from solders.keypair import Keypair
1200
- from solders.message import Message
1201
- from solders.pubkey import Pubkey
1202
- from solders.transaction import Transaction
1203
- from spl.token.instructions import (
1204
- TransferCheckedParams,
1205
- get_associated_token_address,
1206
- transfer_checked,
1207
- )
1208
- except ImportError as exc:
1209
- raise WalletBackendError(
1210
- "solana and solders packages are required for SPL token transfers."
1211
- ) from exc
1212
-
1213
- sender_pubkey = Pubkey.from_string(sender)
1214
- mint_pubkey = Pubkey.from_string(mint)
1215
- token_program_id = await self._resolve_token_program_id(mint)
1216
- token_program_pubkey = Pubkey.from_string(token_program_id)
1217
- sender_token_account = str(
1218
- get_associated_token_address(
1219
- sender_pubkey,
1220
- mint_pubkey,
1221
- token_program_id=token_program_pubkey,
1222
- )
1223
- )
1224
- sender_token_account_exists = await solana_rpc.account_exists(
1225
- sender_token_account,
1226
- rpc_url=self.rpc_urls,
1227
- )
1228
- if not sender_token_account_exists:
1229
- raise WalletBackendError("Sender token account does not exist for this mint.")
1230
- original_deposit_address = recipient_token_account
1231
- await self._wait_for_houdini_spl_deposit_ready(
1232
- deposit_address=recipient_token_account,
1233
- mint=mint,
1234
- )
1235
-
1236
- sender_balance = await solana_rpc.fetch_token_account_balance(
1237
- sender_token_account,
1238
- rpc_url=self.rpc_urls,
1239
- )
1240
- sender_raw_balance = int(sender_balance.get("amount") or 0)
1241
- if amount_raw > sender_raw_balance:
1242
- raise WalletBackendError("Insufficient token balance for this private transfer.")
1243
-
1244
- latest_blockhash = await solana_rpc.fetch_latest_blockhash(
1245
- rpc_url=self.rpc_urls,
1246
- commitment=self.commitment,
1247
- )
1248
- blockhash = Hash.from_string(str(latest_blockhash["blockhash"]))
1249
- keypair = Keypair.from_bytes(self.signer.export_keypair_bytes())
1250
- message = Message.new_with_blockhash(
1251
- [
1252
- transfer_checked(
1253
- TransferCheckedParams(
1254
- program_id=token_program_pubkey,
1255
- source=Pubkey.from_string(sender_token_account),
1256
- mint=mint_pubkey,
1257
- dest=Pubkey.from_string(recipient_token_account),
1258
- owner=sender_pubkey,
1259
- amount=amount_raw,
1260
- decimals=decimals,
1261
- signers=[],
1262
- )
1263
- )
1264
- ],
1265
- sender_pubkey,
1266
- blockhash,
1267
- )
1268
- transaction = Transaction([keypair], message, blockhash)
1269
- submitted = await solana_rpc.send_transaction(
1270
- transaction_base64=encode_transaction_base64(bytes(transaction)),
1271
- rpc_url=self.rpc_urls,
1272
- )
1273
- signature = submitted.get("signature")
1274
- status = None
1275
- confirmed = False
1276
- if isinstance(signature, str) and signature:
1277
- status = await solana_rpc.wait_for_confirmation(
1278
- signature=signature,
1279
- rpc_url=self.rpc_urls,
1280
- )
1281
- confirmed = status is not None
1282
-
1283
- return {
1284
- "chain": "solana",
1285
- "network": self.network,
1286
- "mode": "execute",
1287
- "asset_type": "spl",
1288
- "from_address": sender,
1289
- "to_address": recipient_token_account,
1290
- "requested_deposit_address": original_deposit_address,
1291
- "deposit_address_interpretation": "token_account",
1292
- "mint": mint,
1293
- "token_program_id": token_program_id,
1294
- "sender_token_account": sender_token_account,
1295
- "recipient_token_account": recipient_token_account,
1296
- "recipient_token_account_exists_before": True,
1297
- "recipient_token_account_created": False,
1298
- "amount_ui": float(Decimal(amount_raw) / (Decimal(10) ** decimals)),
1299
- "amount_raw": amount_raw,
1300
- "decimals": decimals,
1301
- "signature": signature,
1302
- "broadcasted": bool(signature),
1303
- "confirmed": confirmed,
1304
- "confirmation_status": status.get("confirmationStatus") if status else None,
1305
- "slot": status.get("slot") if status else None,
1306
- "sign_only": self.sign_only,
1307
- "source": "solana-rpc",
1308
- "execute_response": submitted,
1309
- }
1310
-
1311
- async def execute_solana_private_swap(
1312
- self,
1313
- *,
1314
- input_token: str,
1315
- output_token: str,
1316
- destination_address: str,
1317
- amount_ui: float,
1318
- use_xmr: bool = False,
1319
- approved_preview: dict[str, Any] | None = None,
1320
- existing_order: dict[str, Any] | None = None,
1321
- ) -> dict[str, Any]:
1322
- if self.network != "mainnet":
1323
- raise WalletBackendError("Houdini private Solana swaps are only enabled for Solana mainnet.")
1324
- if not self.signer:
1325
- raise WalletBackendError("Solana signer is not configured.")
1326
- if self.sign_only:
1327
- raise WalletBackendError(
1328
- "This wallet backend is in sign-only mode. Disable sign_only to broadcast transactions."
1329
- )
1330
-
1331
- preview = (
1332
- dict(approved_preview)
1333
- if isinstance(approved_preview, dict)
1334
- else await self.preview_solana_private_swap(
1335
- input_token=input_token,
1336
- output_token=output_token,
1337
- destination_address=destination_address,
1338
- amount_ui=amount_ui,
1339
- use_xmr=use_xmr,
1340
- )
1341
- )
1342
- owner = str(preview.get("owner") or await self.get_address() or "").strip()
1343
- if not owner:
1344
- raise WalletBackendError("No Solana wallet address configured for Houdini execution.")
1345
-
1346
- quote_id = str(preview.get("quote_id") or "").strip()
1347
- if not quote_id:
1348
- raise WalletBackendError("Approved private swap preview is missing quote_id.")
1349
-
1350
- if isinstance(existing_order, dict) and existing_order:
1351
- create_payload = dict(existing_order)
1352
- order = (
1353
- create_payload.get("order")
1354
- if isinstance(create_payload.get("order"), dict)
1355
- else create_payload
1356
- )
1357
- if not isinstance(order, dict):
1358
- raise WalletBackendError("Stored Houdini private swap order is invalid.")
1359
- multi_id = str(create_payload.get("multi_id") or create_payload.get("multiId") or order.get("multiId") or "").strip() or None
1360
- houdini_id = str(
1361
- create_payload.get("houdini_id")
1362
- or create_payload.get("houdiniId")
1363
- or order.get("houdiniId")
1364
- or ""
1365
- ).strip()
1366
- if not houdini_id:
1367
- raise WalletBackendError("Stored Houdini private swap order is missing houdini_id.")
1368
- latest_order = await houdini.fetch_order_status(houdini_id=houdini_id)
1369
- if isinstance(latest_order, dict) and latest_order:
1370
- order = latest_order
1371
-
1372
- order_validation = self._validate_houdini_order_against_preview(
1373
- order=order,
1374
- preview=preview,
1375
- )
1376
- output_validation = self._validate_houdini_order_output_against_preview(
1377
- order=order,
1378
- preview=preview,
1379
- )
1380
- input_decimals = int(preview.get("input_token_decimals") or (9 if bool(preview.get("input_is_native")) else 0))
1381
- input_amount_raw = int(
1382
- (Decimal(str(preview["input_amount_ui"])) * (Decimal(10) ** input_decimals))
1383
- .to_integral_value()
1384
- )
1385
- deposit_address = str(order.get("depositAddress") or "").strip()
1386
- if not deposit_address:
1387
- raise WalletBackendError("Houdini private swap response is missing depositAddress.")
1388
-
1389
- if bool(preview.get("input_is_native")):
1390
- funding_result = await self.send_native_transfer(
1391
- recipient=deposit_address,
1392
- amount_native=float(Decimal(str(preview["input_amount_ui"]))),
1393
- )
1394
- else:
1395
- try:
1396
- funding_result = await self._send_houdini_exact_spl_deposit(
1397
- recipient_token_account=deposit_address,
1398
- mint=str(preview.get("input_token_address") or "").strip(),
1399
- amount_raw=input_amount_raw,
1400
- decimals=input_decimals,
1401
- )
1402
- except WalletBackendError as exc:
1403
- if exc.code == "houdini_deposit_not_ready":
1404
- details = dict(exc.details or {})
1405
- details.update(
1406
- {
1407
- "multi_id": multi_id,
1408
- "houdini_id": houdini_id,
1409
- "deposit_address": deposit_address,
1410
- "order_status": order.get("statusLabel"),
1411
- "order": order,
1412
- "input_token_address": str(preview.get("input_token_address") or "").strip(),
1413
- "input_amount_raw": str(input_amount_raw),
1414
- "input_decimals": input_decimals,
1415
- }
1416
- )
1417
- raise WalletBackendError(
1418
- "Houdini order exists, but its Solana deposit account is not ready yet. Retry continue for the existing order instead of generating a new preview.",
1419
- code="houdini_deposit_not_ready",
1420
- details=details,
1421
- ) from exc
1422
- raise
1423
-
1424
- signature = str(funding_result.get("signature") or "").strip() or None
1425
- confirmed = bool(funding_result.get("confirmed"))
1426
-
1427
- return {
1428
- "chain": "solana",
1429
- "network": self.network,
1430
- "mode": "execute",
1431
- "asset_type": "solana-private-swap",
1432
- "owner": owner,
1433
- "destination_address": str(preview["destination_address"]),
1434
- "input_token_id": preview["input_token_id"],
1435
- "output_token_id": preview["output_token_id"],
1436
- "input_token_symbol": preview["input_token_symbol"],
1437
- "output_token_symbol": preview["output_token_symbol"],
1438
- "input_token_address": preview["input_token_address"],
1439
- "output_token_address": preview["output_token_address"],
1440
- "input_is_native": bool(preview.get("input_is_native")),
1441
- "input_amount_ui": preview["input_amount_ui"],
1442
- "estimated_output_amount_ui": preview["estimated_output_amount_ui"],
1443
- "private_duration_minutes": order.get("eta") or preview.get("private_duration_minutes"),
1444
- "multi_id": multi_id,
1445
- "houdini_id": houdini_id,
1446
- "deposit_address": deposit_address,
1447
- "order_status": order.get("statusLabel"),
1448
- "order": order,
1449
- "signature": signature,
1450
- "broadcasted": bool(signature),
1451
- "confirmed": confirmed,
1452
- "confirmation_status": funding_result.get("confirmation_status"),
1453
- "slot": funding_result.get("slot"),
1454
- "verification": {
1455
- "verified": True,
1456
- "deposit_address": deposit_address,
1457
- "amount_raw": str(input_amount_raw),
1458
- "is_native_input": bool(preview.get("input_is_native")),
1459
- "token_mint": (
1460
- None
1461
- if bool(preview.get("input_is_native"))
1462
- else str(preview.get("input_token_address") or "").strip()
1463
- ),
1464
- "quote_bound_single_exchange": True,
1465
- },
1466
- "simulation": None,
1467
- "provider_order_validation": order_validation,
1468
- "output_validation": output_validation,
1469
- "funding_transfer": funding_result,
1470
- "execute_response": funding_result.get("execute_response"),
1471
- "status_tracking": {
1472
- "multi_id": multi_id,
1473
- "houdini_id": houdini_id,
1474
- "poll_status_tool": "get_solana_private_swap_status",
1475
- },
1476
- "source": "houdini",
1477
- "execution_state": "funding_submitted",
1478
- }
1479
- else:
1480
- try:
1481
- create_payload = await houdini.create_exchange(
1482
- quote_id=quote_id,
1483
- destination_address=str(preview["destination_address"]),
1484
- )
1485
- except ProviderError as exc:
1486
- details = dict(exc.details or {})
1487
- gateway_error = details.get("error") if isinstance(details.get("error"), dict) else None
1488
- if isinstance(gateway_error, dict) and str(gateway_error.get("code") or "").strip() == "RATE_LIMIT_EXCEEDED":
1489
- retry_after = gateway_error.get("retryAfter")
1490
- raise WalletBackendError(
1491
- "Houdini exchange create is rate-limited right now. Wait and retry execute without generating a new preview.",
1492
- code="houdini_exchange_rate_limited",
1493
- details={
1494
- "retry_after": retry_after,
1495
- "quote_id": quote_id,
1496
- "destination_address": str(preview["destination_address"]),
1497
- "provider": getattr(exc, "provider", "houdini"),
1498
- "upstream_error": gateway_error,
1499
- },
1500
- ) from exc
1501
- raise
1502
- order = create_payload.get("order") if isinstance(create_payload.get("order"), dict) else create_payload
1503
- if not isinstance(order, dict):
1504
- raise WalletBackendError("Houdini returned no order object for the private swap.")
1505
- if isinstance(order.get("error"), dict):
1506
- error = order["error"]
1507
- raise WalletBackendError(
1508
- f"Houdini rejected the private swap request: {error.get('message') or 'unknown error'}.",
1509
- code=str(error.get("code") or "").strip() or None,
1510
- details=error,
1511
- )
1512
-
1513
- multi_id = str(create_payload.get("multiId") or order.get("multiId") or "").strip() or None
1514
- houdini_id = str(order.get("houdiniId") or create_payload.get("houdiniId") or "").strip()
1515
- if not houdini_id:
1516
- raise WalletBackendError("Houdini private swap response is missing the order identifier.")
1517
- deposit_address = str(order.get("depositAddress") or "").strip()
1518
- if not deposit_address:
1519
- raise WalletBackendError("Houdini private swap response is missing depositAddress.")
1520
-
1521
- order_validation = self._validate_houdini_order_against_preview(
1522
- order=order,
1523
- preview=preview,
1524
- )
1525
- output_validation = self._validate_houdini_order_output_against_preview(
1526
- order=order,
1527
- preview=preview,
1528
- )
1529
-
1530
- return {
1531
- "chain": "solana",
1532
- "network": self.network,
1533
- "mode": "execute",
1534
- "asset_type": "solana-private-swap",
1535
- "owner": owner,
1536
- "destination_address": str(preview["destination_address"]),
1537
- "input_token_id": preview["input_token_id"],
1538
- "output_token_id": preview["output_token_id"],
1539
- "input_token_symbol": preview["input_token_symbol"],
1540
- "output_token_symbol": preview["output_token_symbol"],
1541
- "input_token_address": preview["input_token_address"],
1542
- "output_token_address": preview["output_token_address"],
1543
- "input_is_native": bool(preview.get("input_is_native")),
1544
- "input_amount_ui": preview["input_amount_ui"],
1545
- "estimated_output_amount_ui": preview["estimated_output_amount_ui"],
1546
- "private_duration_minutes": order.get("eta") or preview.get("private_duration_minutes"),
1547
- "multi_id": multi_id,
1548
- "houdini_id": houdini_id,
1549
- "deposit_address": deposit_address,
1550
- "order_status": order.get("statusLabel"),
1551
- "order": order,
1552
- "provider_order_validation": order_validation,
1553
- "output_validation": output_validation,
1554
- "status_tracking": {
1555
- "multi_id": multi_id,
1556
- "houdini_id": houdini_id,
1557
- "poll_status_tool": "get_solana_private_swap_status",
1558
- },
1559
- "source": "houdini",
1560
- "execution_state": "awaiting_deposit_funding",
1561
- "next_step": "Call continue_solana_private_swap with the same approved private swap context to submit the funding transfer.",
1562
- }
1563
-
1564
- async def continue_solana_private_swap(
1565
- self,
1566
- *,
1567
- approved_preview: dict[str, Any],
1568
- existing_order: dict[str, Any],
1569
- ) -> dict[str, Any]:
1570
- return await self.execute_solana_private_swap(
1571
- input_token=str(approved_preview.get("input_token_query") or approved_preview.get("input_token_symbol") or ""),
1572
- output_token=str(approved_preview.get("output_token_query") or approved_preview.get("output_token_symbol") or ""),
1573
- destination_address=str(approved_preview.get("destination_address") or ""),
1574
- amount_ui=float(approved_preview.get("input_amount_ui") or 0),
1575
- use_xmr=bool(approved_preview.get("use_xmr", False)),
1576
- approved_preview=approved_preview,
1577
- existing_order=existing_order,
1578
- )
1579
-
1580
723
  async def preview_solana_lifi_cross_chain_swap(
1581
724
  self,
1582
725
  *,
@@ -2459,76 +1602,6 @@ class SolanaWalletBackend(AgentWalletBackend):
2459
1602
  "source": "jupiter-portfolio",
2460
1603
  }
2461
1604
 
2462
- async def get_jupiter_earn_tokens(self) -> dict[str, Any]:
2463
- self._require_mainnet_jupiter("Jupiter Earn")
2464
- data = await jupiter.fetch_earn_tokens()
2465
- tokens = data.get("tokens")
2466
- if not isinstance(tokens, list):
2467
- tokens = []
2468
- return {
2469
- "chain": "solana",
2470
- "network": self.network,
2471
- "token_count": len(tokens),
2472
- "tokens": tokens,
2473
- "raw": data,
2474
- "source": "jupiter-lend",
2475
- }
2476
-
2477
- async def get_jupiter_earn_positions(
2478
- self,
2479
- users: list[str] | None = None,
2480
- ) -> dict[str, Any]:
2481
- self._require_mainnet_jupiter("Jupiter Earn")
2482
- resolved_users = users or [self.address]
2483
- if not resolved_users or any(user is None for user in resolved_users):
2484
- raise WalletBackendError("At least one wallet address is required for Earn positions.")
2485
- normalized_users = [validate_solana_address(str(user)) for user in resolved_users]
2486
- data = await jupiter.fetch_earn_positions(users=normalized_users)
2487
- positions = data.get("positions")
2488
- if not isinstance(positions, list):
2489
- positions = []
2490
- return {
2491
- "chain": "solana",
2492
- "network": self.network,
2493
- "users": normalized_users,
2494
- "position_count": len(positions),
2495
- "positions": positions,
2496
- "raw": data,
2497
- "source": "jupiter-lend",
2498
- }
2499
-
2500
- async def get_jupiter_earn_earnings(
2501
- self,
2502
- user: str | None = None,
2503
- positions: list[str] | None = None,
2504
- ) -> dict[str, Any]:
2505
- self._require_mainnet_jupiter("Jupiter Earn")
2506
- wallet_address = user or self.address
2507
- if not wallet_address:
2508
- raise WalletBackendError(
2509
- "A wallet address is required for Jupiter Earn earnings lookup."
2510
- )
2511
- if not positions:
2512
- raise WalletBackendError("positions must include at least one Earn position address.")
2513
- wallet_address = validate_solana_address(wallet_address)
2514
- normalized_positions = [validate_solana_address(str(position)) for position in positions]
2515
- data = await jupiter.fetch_earn_earnings(
2516
- user=wallet_address,
2517
- positions=normalized_positions,
2518
- )
2519
- earnings = data.get("earnings")
2520
- if not isinstance(earnings, list):
2521
- earnings = []
2522
- return {
2523
- "chain": "solana",
2524
- "network": self.network,
2525
- "user": wallet_address,
2526
- "positions": normalized_positions,
2527
- "earnings": earnings,
2528
- "raw": data,
2529
- "source": "jupiter-lend",
2530
- }
2531
-
2532
1605
  async def get_flash_trade_markets(
2533
1606
  self,
2534
1607
  pool_name: str | None = None,
@@ -4226,53 +3299,6 @@ class SolanaWalletBackend(AgentWalletBackend):
4226
3299
  )
4227
3300
  return encode_transaction_base64(bytes(signed_transaction))
4228
3301
 
4229
- async def _prepare_jupiter_lend_transaction(
4230
- self,
4231
- *,
4232
- transaction_base64: str,
4233
- action: str,
4234
- asset: str,
4235
- amount_raw: str,
4236
- ) -> dict[str, Any]:
4237
- if not self.signer:
4238
- raise WalletBackendError("Solana signer is not configured.")
4239
- try:
4240
- from solders.transaction import VersionedTransaction
4241
- except ImportError as exc:
4242
- raise WalletBackendError(
4243
- "solana and solders packages are required for Jupiter Earn transaction signing."
4244
- ) from exc
4245
- unsigned_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_base64))
4246
- owner = await self.get_address()
4247
- verification = verify_provider_lend_transaction(
4248
- unsigned_transaction.message,
4249
- wallet_address=str(owner),
4250
- asset_mint=asset,
4251
- action=f"Jupiter Earn {action}",
4252
- )
4253
- signed_transaction_base64 = await self._sign_versioned_provider_transaction(
4254
- transaction_base64=transaction_base64,
4255
- wallet_signer_index=int(verification.get("wallet_signer_index") or 0),
4256
- )
4257
- return {
4258
- "chain": "solana",
4259
- "network": self.network,
4260
- "mode": "prepare",
4261
- "asset_type": f"jupiter-earn-{action}",
4262
- "owner": owner,
4263
- "asset": asset,
4264
- "amount_raw": amount_raw,
4265
- "transaction_base64": signed_transaction_base64,
4266
- "transaction_encoding": "base64",
4267
- "transaction_format": "versioned",
4268
- "signed": True,
4269
- "broadcasted": False,
4270
- "confirmed": False,
4271
- "verification": verification,
4272
- "sign_only": self.sign_only,
4273
- "source": "jupiter-lend",
4274
- }
4275
-
4276
3302
  async def _execute_prepared_provider_transaction(
4277
3303
  self,
4278
3304
  prepared: dict[str, Any],
@@ -4319,12 +3345,6 @@ class SolanaWalletBackend(AgentWalletBackend):
4319
3345
  "kamino_safety": prepared.get("kamino_safety"),
4320
3346
  }
4321
3347
 
4322
- async def _execute_prepared_jupiter_lend_transaction(self, prepared: dict[str, Any]) -> dict[str, Any]:
4323
- return await self._execute_prepared_provider_transaction(
4324
- prepared,
4325
- source="jupiter-lend",
4326
- )
4327
-
4328
3348
  def _find_kamino_reserve_entry(
4329
3349
  self,
4330
3350
  *,
@@ -4965,143 +3985,6 @@ class SolanaWalletBackend(AgentWalletBackend):
4965
3985
  result["build_response"] = prepared.get("build_response")
4966
3986
  return result
4967
3987
 
4968
- async def preview_jupiter_earn_deposit(
4969
- self,
4970
- asset: str,
4971
- amount_raw: str,
4972
- ) -> dict[str, Any]:
4973
- self._require_mainnet_jupiter("Jupiter Earn")
4974
- owner = await self.get_address()
4975
- if not owner:
4976
- raise WalletBackendError(
4977
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
4978
- )
4979
- amount_raw = _require_positive_integer_string(amount_raw, field_name="amount_raw")
4980
- asset = validate_solana_mint(asset)
4981
- tokens = await self.get_jupiter_earn_tokens()
4982
- token_entry = next(
4983
- (
4984
- item
4985
- for item in tokens["tokens"]
4986
- if isinstance(item, dict)
4987
- and str(item.get("asset") or item.get("mint") or "").strip() == asset
4988
- ),
4989
- None,
4990
- )
4991
- if token_entry is None:
4992
- raise WalletBackendError("Requested asset is not currently available in Jupiter Earn.")
4993
- return {
4994
- "chain": "solana",
4995
- "network": self.network,
4996
- "mode": "preview",
4997
- "asset_type": "jupiter-earn-deposit",
4998
- "owner": owner,
4999
- "asset": asset,
5000
- "amount_raw": amount_raw,
5001
- "token": token_entry,
5002
- "sign_only": self.sign_only,
5003
- "can_send": self.get_capabilities().can_send_transaction,
5004
- "source": "jupiter-lend",
5005
- }
5006
-
5007
- async def prepare_jupiter_earn_deposit(
5008
- self,
5009
- asset: str,
5010
- amount_raw: str,
5011
- ) -> dict[str, Any]:
5012
- preview = await self.preview_jupiter_earn_deposit(asset=asset, amount_raw=amount_raw)
5013
- owner = str(preview["owner"])
5014
- build = await jupiter.build_earn_deposit_transaction(
5015
- asset=str(preview["asset"]),
5016
- user_address=owner,
5017
- amount_raw=str(preview["amount_raw"]),
5018
- )
5019
- prepared = await self._prepare_jupiter_lend_transaction(
5020
- transaction_base64=str(build["transaction"]),
5021
- action="deposit",
5022
- asset=str(preview["asset"]),
5023
- amount_raw=str(preview["amount_raw"]),
5024
- )
5025
- prepared["build_response"] = build
5026
- return prepared
5027
-
5028
- async def execute_jupiter_earn_deposit(
5029
- self,
5030
- asset: str,
5031
- amount_raw: str,
5032
- ) -> dict[str, Any]:
5033
- prepared = await self.prepare_jupiter_earn_deposit(asset=asset, amount_raw=amount_raw)
5034
- result = await self._execute_prepared_jupiter_lend_transaction(prepared)
5035
- result["build_response"] = prepared.get("build_response")
5036
- return result
5037
-
5038
- async def preview_jupiter_earn_withdraw(
5039
- self,
5040
- asset: str,
5041
- amount_raw: str,
5042
- ) -> dict[str, Any]:
5043
- self._require_mainnet_jupiter("Jupiter Earn")
5044
- owner = await self.get_address()
5045
- if not owner:
5046
- raise WalletBackendError(
5047
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
5048
- )
5049
- amount_raw = _require_positive_integer_string(amount_raw, field_name="amount_raw")
5050
- asset = validate_solana_mint(asset)
5051
- positions = await self.get_jupiter_earn_positions(users=[owner])
5052
- matching_positions = [
5053
- item
5054
- for item in positions["positions"]
5055
- if isinstance(item, dict)
5056
- and str(item.get("asset") or item.get("mint") or "").strip() == asset
5057
- ]
5058
- if not matching_positions:
5059
- raise WalletBackendError("No Jupiter Earn position found for the requested asset.")
5060
- return {
5061
- "chain": "solana",
5062
- "network": self.network,
5063
- "mode": "preview",
5064
- "asset_type": "jupiter-earn-withdraw",
5065
- "owner": owner,
5066
- "asset": asset,
5067
- "amount_raw": amount_raw,
5068
- "positions": matching_positions,
5069
- "sign_only": self.sign_only,
5070
- "can_send": self.get_capabilities().can_send_transaction,
5071
- "source": "jupiter-lend",
5072
- }
5073
-
5074
- async def prepare_jupiter_earn_withdraw(
5075
- self,
5076
- asset: str,
5077
- amount_raw: str,
5078
- ) -> dict[str, Any]:
5079
- preview = await self.preview_jupiter_earn_withdraw(asset=asset, amount_raw=amount_raw)
5080
- owner = str(preview["owner"])
5081
- build = await jupiter.build_earn_withdraw_transaction(
5082
- asset=str(preview["asset"]),
5083
- user_address=owner,
5084
- amount_raw=str(preview["amount_raw"]),
5085
- )
5086
- prepared = await self._prepare_jupiter_lend_transaction(
5087
- transaction_base64=str(build["transaction"]),
5088
- action="withdraw",
5089
- asset=str(preview["asset"]),
5090
- amount_raw=str(preview["amount_raw"]),
5091
- )
5092
- prepared["build_response"] = build
5093
- return prepared
5094
-
5095
- async def execute_jupiter_earn_withdraw(
5096
- self,
5097
- asset: str,
5098
- amount_raw: str,
5099
- ) -> dict[str, Any]:
5100
- prepared = await self.prepare_jupiter_earn_withdraw(asset=asset, amount_raw=amount_raw)
5101
- result = await self._execute_prepared_jupiter_lend_transaction(prepared)
5102
- result["build_response"] = prepared.get("build_response")
5103
- return result
5104
-
5105
3988
  async def preview_native_transfer(
5106
3989
  self,
5107
3990
  recipient: str,