@agentlayer.tech/wallet 0.1.28 → 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 (71) hide show
  1. package/.openclaw/extensions/agent-wallet/README.md +5 -7
  2. package/.openclaw/extensions/agent-wallet/dist/index.js +35 -360
  3. package/.openclaw/extensions/agent-wallet/index.ts +35 -360
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +2 -45
  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 +73 -0
  8. package/README.md +4 -0
  9. package/agent-wallet/.env.example +0 -12
  10. package/agent-wallet/README.md +18 -57
  11. package/agent-wallet/agent_wallet/bootstrap.py +28 -12
  12. package/agent-wallet/agent_wallet/btc_user_wallets.py +33 -7
  13. package/agent-wallet/agent_wallet/config.py +110 -29
  14. package/agent-wallet/agent_wallet/evm_user_wallets.py +4 -14
  15. package/agent-wallet/agent_wallet/openclaw_adapter.py +29 -687
  16. package/agent-wallet/agent_wallet/openclaw_cli.py +0 -7
  17. package/agent-wallet/agent_wallet/openclaw_runtime.py +3 -12
  18. package/agent-wallet/agent_wallet/providers/evm_portfolio.py +18 -42
  19. package/agent-wallet/agent_wallet/providers/jupiter.py +1 -307
  20. package/agent-wallet/agent_wallet/providers/kamino.py +21 -4
  21. package/agent-wallet/agent_wallet/providers/solana_rpc.py +0 -23
  22. package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +31 -3
  23. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +37 -3
  24. package/agent-wallet/agent_wallet/providers/x402.py +4 -9
  25. package/agent-wallet/agent_wallet/transaction_policy.py +0 -262
  26. package/agent-wallet/agent_wallet/user_wallets.py +4 -3
  27. package/agent-wallet/agent_wallet/wallet_layer/base.py +3 -103
  28. package/agent-wallet/agent_wallet/wallet_layer/factory.py +8 -5
  29. package/agent-wallet/agent_wallet/wallet_layer/solana.py +453 -1177
  30. package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +2 -8
  31. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +2 -12
  32. package/agent-wallet/examples/openclaw_runtime_onboarding.py +1 -1
  33. package/agent-wallet/examples/openclaw_user_wallet_example.py +1 -1
  34. package/agent-wallet/openclaw.plugin.json +1 -5
  35. package/agent-wallet/pyproject.toml +2 -1
  36. package/agent-wallet/scripts/bootstrap_openclaw_btc.py +3 -5
  37. package/agent-wallet/scripts/bootstrap_openclaw_evm.py +2 -12
  38. package/agent-wallet/scripts/build_release_bundle.py +1 -0
  39. package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1 -4
  40. package/agent-wallet/scripts/install_agent_wallet.py +114 -6
  41. package/agent-wallet/scripts/install_openclaw_local_config.py +10 -10
  42. package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +2 -4
  43. package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +2 -15
  44. package/agent-wallet/scripts/reveal_btc_seed.sh +7 -16
  45. package/agent-wallet/scripts/setup_btc_wallet.sh +7 -16
  46. package/agent-wallet/scripts/setup_evm_wallet.sh +1 -11
  47. package/agent-wallet/scripts/switch_openclaw_wallet_network.py +4 -1
  48. package/agent-wallet/skills/wallet-operator/SKILL.md +1 -6
  49. package/bin/openclaw-agent-wallet.mjs +356 -0
  50. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +20 -0
  51. package/claude-code/plugins/agent-wallet/.mcp.json +14 -0
  52. package/claude-code/plugins/agent-wallet/README.md +65 -0
  53. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +39 -0
  54. package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
  55. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +38 -0
  56. package/codex/plugins/agent-wallet/.mcp.json +15 -0
  57. package/codex/plugins/agent-wallet/README.md +39 -0
  58. package/codex/plugins/agent-wallet/scripts/run_mcp.sh +21 -0
  59. package/codex/plugins/agent-wallet/server.py +961 -0
  60. package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
  61. package/hermes/plugins/agent_wallet/schemas.py +2 -2
  62. package/hermes/plugins/agent_wallet/tools.py +18 -4
  63. package/package.json +6 -1
  64. package/setup.sh +2 -0
  65. package/wdk-btc-wallet/src/local_vault.js +45 -68
  66. package/wdk-btc-wallet/src/server.js +1 -0
  67. package/wdk-evm-wallet/README.md +4 -3
  68. package/wdk-evm-wallet/src/config.js +15 -0
  69. package/wdk-evm-wallet/src/local_vault.js +45 -68
  70. package/wdk-evm-wallet/src/server.js +1 -0
  71. package/agent-wallet/agent_wallet/providers/houdini.py +0 -539
@@ -11,8 +11,9 @@ import time
11
11
  from decimal import Decimal, InvalidOperation
12
12
  from typing import Any
13
13
 
14
+ from agent_wallet.config import normalize_solana_network
14
15
  from agent_wallet.models import AgentWalletCapabilities, SolanaWalletState
15
- 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
16
17
  from agent_wallet.solana_stake import (
17
18
  STAKE_STATE_V2_SIZE,
18
19
  deactivate_stake as build_deactivate_stake_instruction,
@@ -29,7 +30,6 @@ from agent_wallet.transaction_policy import (
29
30
  verify_provider_bags_transaction,
30
31
  verify_provider_flash_transaction,
31
32
  verify_provider_kamino_lend_transaction,
32
- verify_provider_lend_transaction,
33
33
  verify_provider_swap_simulation_result,
34
34
  verify_provider_swap_transaction,
35
35
  )
@@ -48,9 +48,9 @@ TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
48
48
  TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
49
49
  NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112"
50
50
  STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111"
51
- HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS = 600
52
51
  SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS = 300
53
52
  SOLANA_SWAP_INTENT_DEFAULT_MAX_FEE_LAMPORTS = 6_000_000
53
+ KAMINO_OPEN_POSITIONS_SCAN_CONCURRENCY = 6
54
54
 
55
55
 
56
56
  def _load_signing_key():
@@ -275,7 +275,7 @@ class SolanaWalletBackend(AgentWalletBackend):
275
275
  self.rpc_urls = rpc_url if isinstance(rpc_url, list) else [rpc_url]
276
276
  self.rpc_url = self.rpc_urls[0]
277
277
  self.commitment = commitment
278
- self.network = network
278
+ self.network = normalize_solana_network(network)
279
279
  self.signer = signer
280
280
  self.address = final_address
281
281
  self.sign_only = sign_only
@@ -720,861 +720,6 @@ class SolanaWalletBackend(AgentWalletBackend):
720
720
  }
721
721
 
722
722
  @staticmethod
723
- def _houdini_token_is_native(token: dict[str, Any]) -> bool:
724
- chain = str(token.get("chain") or "").strip().lower()
725
- symbol = str(token.get("symbol") or "").strip().lower()
726
- address = str(token.get("address") or "").strip().lower()
727
- return chain == "solana" and symbol == "sol" and address in {"", "sol", "native", "11111111111111111111111111111111", NATIVE_SOL_MINT.lower()}
728
-
729
- def _normalize_houdini_private_preview_payload(
730
- self,
731
- *,
732
- owner: str,
733
- input_token: dict[str, Any],
734
- output_token: dict[str, Any],
735
- destination_address: str,
736
- amount_ui: Decimal,
737
- quote: dict[str, Any],
738
- use_xmr: bool,
739
- ) -> dict[str, Any]:
740
- min_private = (
741
- input_token.get("min_max_private")
742
- if isinstance(input_token.get("min_max_private"), dict)
743
- else {}
744
- )
745
- return {
746
- "chain": "solana",
747
- "network": self.network,
748
- "mode": "preview",
749
- "asset_type": "solana-private-swap",
750
- "owner": owner,
751
- "destination_address": destination_address,
752
- "input_token_query": input_token.get("symbol"),
753
- "output_token_query": output_token.get("symbol"),
754
- "input_token_id": input_token.get("id"),
755
- "output_token_id": output_token.get("id"),
756
- "input_token_symbol": input_token.get("symbol"),
757
- "output_token_symbol": output_token.get("symbol"),
758
- "input_token_name": input_token.get("name"),
759
- "output_token_name": output_token.get("name"),
760
- "input_token_address": input_token.get("address"),
761
- "output_token_address": output_token.get("address"),
762
- "input_token_chain": input_token.get("chain"),
763
- "output_token_chain": output_token.get("chain"),
764
- "input_token_decimals": input_token.get("decimals"),
765
- "output_token_decimals": output_token.get("decimals"),
766
- "input_is_native": self._houdini_token_is_native(input_token),
767
- "output_is_native": self._houdini_token_is_native(output_token),
768
- "input_amount_ui": float(amount_ui),
769
- "estimated_output_amount_ui": float(Decimal(str(quote.get("amountOut") or "0"))),
770
- "estimated_output_amount_usd": quote.get("amountOutUsd"),
771
- "input_private_min_ui": min_private.get("min"),
772
- "input_private_max_ui": min_private.get("max"),
773
- "private_duration_minutes": quote.get("duration"),
774
- "quote_id": quote.get("quoteId"),
775
- "quote_type": quote.get("type"),
776
- "rewards_available": quote.get("rewardsAvailable"),
777
- "anonymous": True,
778
- "use_xmr": bool(use_xmr),
779
- "can_send": self.get_capabilities().can_send_transaction,
780
- "sign_only": self.sign_only,
781
- "source": "houdini",
782
- }
783
-
784
- async def _wait_for_houdini_order_ready(
785
- self,
786
- *,
787
- multi_id: str,
788
- houdini_id: str,
789
- timeout_seconds: float = 20.0,
790
- poll_interval_seconds: float = 2.0,
791
- ) -> dict[str, Any]:
792
- deadline = asyncio.get_running_loop().time() + timeout_seconds
793
- last_order: dict[str, Any] | None = None
794
- while True:
795
- status_payload = await houdini.fetch_multi_status(multi_id=multi_id)
796
- orders = status_payload.get("orders")
797
- if isinstance(orders, list):
798
- for candidate in orders:
799
- if (
800
- isinstance(candidate, dict)
801
- and str(candidate.get("houdiniId") or "").strip() == houdini_id
802
- ):
803
- last_order = candidate
804
- break
805
- if last_order is None:
806
- raise WalletBackendError("Houdini order disappeared before funding could start.")
807
- if str(last_order.get("statusLabel") or "").strip().upper() != "INITIALIZING":
808
- return last_order
809
- if asyncio.get_running_loop().time() >= deadline:
810
- raise WalletBackendError(
811
- "Houdini order stayed in INITIALIZING too long. Generate a new preview and try again."
812
- )
813
- await asyncio.sleep(poll_interval_seconds)
814
-
815
- async def _wait_for_houdini_single_order_ready(
816
- self,
817
- *,
818
- houdini_id: str,
819
- initial_order: dict[str, Any] | None = None,
820
- timeout_seconds: float = 180.0,
821
- poll_interval_seconds: float = 5.0,
822
- ) -> dict[str, Any]:
823
- deadline = asyncio.get_running_loop().time() + timeout_seconds
824
- last_order: dict[str, Any] | None = initial_order if isinstance(initial_order, dict) else None
825
- while True:
826
- order = await houdini.fetch_order_status(houdini_id=houdini_id)
827
- if isinstance(order, dict) and order:
828
- last_order = order
829
- if str(order.get("statusLabel") or "").strip().upper() != "INITIALIZING":
830
- return order
831
- if asyncio.get_running_loop().time() >= deadline:
832
- details = {
833
- "houdini_id": houdini_id,
834
- "multi_id": str((last_order or {}).get("multiId") or "").strip() or None,
835
- "deposit_address": str((last_order or {}).get("depositAddress") or "").strip() or None,
836
- "order_status": str((last_order or {}).get("statusLabel") or "").strip() or "INITIALIZING",
837
- "order": last_order or order,
838
- }
839
- raise WalletBackendError(
840
- "Houdini order stayed in INITIALIZING too long. Keep the created order and retry execute after the deposit account is ready.",
841
- code="houdini_order_initializing_timeout",
842
- details=details,
843
- )
844
- await asyncio.sleep(poll_interval_seconds)
845
-
846
- async def _wait_for_houdini_spl_deposit_ready(
847
- self,
848
- *,
849
- deposit_address: str,
850
- mint: str,
851
- timeout_seconds: float = 180.0,
852
- poll_interval_seconds: float = 5.0,
853
- ) -> dict[str, Any]:
854
- deadline = asyncio.get_running_loop().time() + timeout_seconds
855
- while True:
856
- account_info = await solana_rpc.fetch_account_info(
857
- deposit_address,
858
- rpc_url=self.rpc_urls,
859
- )
860
- if account_info:
861
- recipient_mint = (
862
- (account_info or {})
863
- .get("data", {})
864
- .get("parsed", {})
865
- .get("info", {})
866
- .get("mint")
867
- )
868
- if recipient_mint and str(recipient_mint).strip() != mint:
869
- raise WalletBackendError(
870
- "Houdini deposit token account mint does not match the approved input token."
871
- )
872
- return {
873
- "deposit_address": deposit_address,
874
- "account_info": account_info,
875
- "mint": recipient_mint,
876
- }
877
- if asyncio.get_running_loop().time() >= deadline:
878
- raise WalletBackendError(
879
- "Houdini order was created, but the Solana deposit token account was not ready in time. Generate a new preview and try again.",
880
- code="houdini_deposit_not_ready",
881
- details={"deposit_address": deposit_address, "mint": mint},
882
- )
883
- await asyncio.sleep(poll_interval_seconds)
884
-
885
- async def preview_solana_private_swap(
886
- self,
887
- *,
888
- input_token: str,
889
- output_token: str,
890
- destination_address: str,
891
- amount_ui: float,
892
- use_xmr: bool = False,
893
- ) -> dict[str, Any]:
894
- if self.network != "mainnet":
895
- raise WalletBackendError("Houdini private Solana swaps are only enabled for Solana mainnet.")
896
- owner = await self.get_address()
897
- if not owner:
898
- raise WalletBackendError(
899
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
900
- )
901
- if not isinstance(destination_address, str) or not destination_address.strip():
902
- raise WalletBackendError("destination_address is required.")
903
- destination_address = validate_solana_address(destination_address.strip())
904
- if not isinstance(amount_ui, (int, float)) or amount_ui <= 0:
905
- raise WalletBackendError("amount must be a positive number.")
906
-
907
- amount_decimal = Decimal(str(amount_ui))
908
- resolved_input = await houdini.resolve_cex_token(term=input_token, chain="solana")
909
- resolved_output = await houdini.resolve_cex_token(term=output_token, chain="solana")
910
- if str(resolved_input.get("id") or "") != str(resolved_output.get("id") or ""):
911
- raise WalletBackendError(
912
- "The initial Houdini Solana private route supports same-token private payouts only. "
913
- "Use the same token for input and output, for example SOL->SOL or USDC->USDC."
914
- )
915
-
916
- private_quotes = await houdini.fetch_private_quotes(
917
- from_token_id=str(resolved_input["id"]),
918
- to_token_id=str(resolved_output["id"]),
919
- amount_ui=amount_decimal,
920
- )
921
- best_quote = houdini.select_best_private_quote(private_quotes)
922
- return self._normalize_houdini_private_preview_payload(
923
- owner=owner,
924
- input_token=resolved_input,
925
- output_token=resolved_output,
926
- destination_address=destination_address,
927
- amount_ui=amount_decimal,
928
- quote=best_quote,
929
- use_xmr=use_xmr,
930
- )
931
-
932
- async def get_solana_private_swap_status(
933
- self,
934
- *,
935
- multi_id: str | None = None,
936
- houdini_id: str | None = None,
937
- ) -> dict[str, Any]:
938
- if self.network != "mainnet":
939
- raise WalletBackendError("Houdini private Solana swaps are only enabled for Solana mainnet.")
940
- normalized_multi_id = str(multi_id or "").strip()
941
- normalized_houdini_id = str(houdini_id or "").strip()
942
- if not normalized_multi_id and not normalized_houdini_id:
943
- raise WalletBackendError("multi_id or houdini_id is required.")
944
-
945
- if not normalized_multi_id:
946
- selected_order = await houdini.fetch_order_status(houdini_id=normalized_houdini_id)
947
- selected_status = str(selected_order.get("statusLabel") or "").strip() or None
948
- terminal_statuses = {"FINISHED", "FAILED", "EXPIRED", "REFUNDED"}
949
- return {
950
- "chain": "solana",
951
- "network": self.network,
952
- "asset_type": "solana-private-swap",
953
- "multi_id": None,
954
- "order_count": 1,
955
- "orders": [selected_order],
956
- "selected_order": selected_order,
957
- "selected_houdini_id": (
958
- str(selected_order.get("houdiniId") or "").strip() or normalized_houdini_id or None
959
- ),
960
- "selected_status": selected_status,
961
- "all_terminal": bool(selected_status and selected_status.upper() in terminal_statuses),
962
- "source": "houdini",
963
- }
964
-
965
- payload = await houdini.fetch_multi_status(multi_id=normalized_multi_id)
966
- orders = payload.get("orders") if isinstance(payload.get("orders"), list) else []
967
- selected_order = None
968
- if normalized_houdini_id:
969
- for order in orders:
970
- if isinstance(order, dict) and str(order.get("houdiniId") or "").strip() == normalized_houdini_id:
971
- selected_order = order
972
- break
973
- if selected_order is None:
974
- raise WalletBackendError("houdini_id was not found inside the requested multi_id.")
975
- elif len(orders) == 1 and isinstance(orders[0], dict):
976
- selected_order = orders[0]
977
-
978
- terminal_statuses = {"FINISHED", "FAILED", "EXPIRED", "REFUNDED"}
979
- return {
980
- "chain": "solana",
981
- "network": self.network,
982
- "asset_type": "solana-private-swap",
983
- "multi_id": payload.get("multiId") or normalized_multi_id,
984
- "order_count": len([item for item in orders if isinstance(item, dict)]),
985
- "orders": orders,
986
- "selected_order": selected_order,
987
- "selected_houdini_id": (
988
- str(selected_order.get("houdiniId") or "").strip()
989
- if isinstance(selected_order, dict)
990
- else None
991
- ),
992
- "selected_status": (
993
- str(selected_order.get("statusLabel") or "").strip()
994
- if isinstance(selected_order, dict)
995
- else None
996
- ),
997
- "all_terminal": all(
998
- isinstance(item, dict)
999
- and str(item.get("statusLabel") or "").strip().upper() in terminal_statuses
1000
- for item in orders
1001
- )
1002
- if orders
1003
- else False,
1004
- "source": "houdini",
1005
- }
1006
-
1007
- @staticmethod
1008
- def _pick_houdini_order_value(order: dict[str, Any], *keys: str) -> str:
1009
- for key in keys:
1010
- value = order.get(key)
1011
- if isinstance(value, str) and value.strip():
1012
- return value.strip()
1013
- if isinstance(value, (int, float, Decimal)):
1014
- text = str(value).strip()
1015
- if text:
1016
- return text
1017
- return ""
1018
-
1019
- @staticmethod
1020
- def _normalize_houdini_token_address(value: str) -> str:
1021
- normalized = str(value or "").strip()
1022
- if not normalized:
1023
- return ""
1024
- lowered = normalized.lower()
1025
- if lowered in {"sol", "native", "11111111111111111111111111111111"}:
1026
- return NATIVE_SOL_MINT.lower()
1027
- return lowered
1028
-
1029
- def _validate_houdini_order_against_preview(
1030
- self,
1031
- *,
1032
- order: dict[str, Any],
1033
- preview: dict[str, Any],
1034
- ) -> dict[str, Any]:
1035
- if str(order.get("receiverAddress") or "").strip() != str(preview["destination_address"]):
1036
- raise WalletBackendError("Houdini order receiverAddress does not match the approved destination.")
1037
- if not bool(order.get("anonymous")):
1038
- raise WalletBackendError("Houdini order is not marked anonymous as required.")
1039
-
1040
- checks: dict[str, Any] = {
1041
- "receiver_address": str(order.get("receiverAddress") or "").strip(),
1042
- "anonymous": bool(order.get("anonymous")),
1043
- }
1044
- warnings: list[str] = []
1045
-
1046
- expected_input_id = str(preview.get("input_token_id") or "").strip()
1047
- order_input_id = self._pick_houdini_order_value(
1048
- order,
1049
- "from",
1050
- "fromId",
1051
- "inId",
1052
- "inputTokenId",
1053
- "fromTokenId",
1054
- )
1055
- if expected_input_id and order_input_id and order_input_id != expected_input_id:
1056
- raise WalletBackendError("Houdini order input token id does not match the approved token.")
1057
- checks["input_token_id"] = order_input_id or expected_input_id or None
1058
-
1059
- expected_input_amount = Decimal(str(preview.get("input_amount_ui") or "0"))
1060
- order_input_amount = Decimal(
1061
- self._pick_houdini_order_value(order, "inAmount", "amountIn", "inputAmount") or "0"
1062
- )
1063
- if order_input_amount != expected_input_amount:
1064
- raise WalletBackendError("Houdini order input amount does not match the approved preview.")
1065
- checks["input_amount_ui"] = str(order_input_amount)
1066
-
1067
- expected_output_id = str(preview.get("output_token_id") or "").strip()
1068
- order_output_id = self._pick_houdini_order_value(
1069
- order,
1070
- "to",
1071
- "toId",
1072
- "outId",
1073
- "outputTokenId",
1074
- "toTokenId",
1075
- )
1076
- if expected_output_id and order_output_id and order_output_id != expected_output_id:
1077
- raise WalletBackendError("Houdini order output token id does not match the approved token.")
1078
- checks["output_token_id"] = order_output_id or expected_output_id or None
1079
-
1080
- expected_input_address = self._normalize_houdini_token_address(
1081
- str(preview.get("input_token_address") or "")
1082
- )
1083
- order_input_address = self._normalize_houdini_token_address(
1084
- self._pick_houdini_order_value(
1085
- order,
1086
- "fromAddress",
1087
- "inAddress",
1088
- "inputTokenAddress",
1089
- "fromTokenAddress",
1090
- )
1091
- )
1092
- if expected_input_address and order_input_address and order_input_address != expected_input_address:
1093
- raise WalletBackendError("Houdini order input token address does not match the approved token.")
1094
- checks["input_token_address"] = order_input_address or expected_input_address or None
1095
-
1096
- expected_output_address = self._normalize_houdini_token_address(
1097
- str(preview.get("output_token_address") or "")
1098
- )
1099
- order_output_address = self._normalize_houdini_token_address(
1100
- self._pick_houdini_order_value(
1101
- order,
1102
- "toAddress",
1103
- "outAddress",
1104
- "outputTokenAddress",
1105
- "toTokenAddress",
1106
- )
1107
- )
1108
- if expected_output_address and order_output_address and order_output_address != expected_output_address:
1109
- raise WalletBackendError("Houdini order output token address does not match the approved token.")
1110
- checks["output_token_address"] = order_output_address or expected_output_address or None
1111
-
1112
- expected_input_symbol = str(preview.get("input_token_symbol") or "").strip().upper()
1113
- order_input_symbol = self._pick_houdini_order_value(order, "inSymbol", "fromSymbol").upper()
1114
- if expected_input_symbol and order_input_symbol and order_input_symbol != expected_input_symbol:
1115
- warnings.append(
1116
- "Houdini order input token symbol differs from the approved preview display symbol."
1117
- )
1118
- checks["input_token_symbol"] = order_input_symbol or expected_input_symbol or None
1119
-
1120
- expected_output_symbol = str(preview.get("output_token_symbol") or "").strip().upper()
1121
- order_output_symbol = self._pick_houdini_order_value(order, "outSymbol", "toSymbol").upper()
1122
- if expected_output_symbol and order_output_symbol and order_output_symbol != expected_output_symbol:
1123
- warnings.append(
1124
- "Houdini order output token symbol differs from the approved preview display symbol."
1125
- )
1126
- checks["output_token_symbol"] = order_output_symbol or expected_output_symbol or None
1127
-
1128
- return {
1129
- "validated": True,
1130
- "checks": checks,
1131
- "warnings": warnings,
1132
- }
1133
-
1134
- def _validate_houdini_order_output_against_preview(
1135
- self,
1136
- *,
1137
- order: dict[str, Any],
1138
- preview: dict[str, Any],
1139
- ) -> dict[str, Any]:
1140
- expected_output = Decimal(str(preview["estimated_output_amount_ui"]))
1141
- order_output = Decimal(
1142
- self._pick_houdini_order_value(order, "outAmount", "amountOut", "outputAmount") or "0"
1143
- )
1144
- tolerance = (
1145
- expected_output * Decimal(HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS) / Decimal(10_000)
1146
- )
1147
- minimum_allowed = expected_output - tolerance
1148
- if order_output < minimum_allowed:
1149
- raise WalletBackendError(
1150
- "Houdini order output fell materially below the approved preview. Generate a new preview and approval before execute.",
1151
- code="private_swap_quote_changed",
1152
- details={
1153
- "approved_estimated_output_amount_ui": str(expected_output),
1154
- "order_output_amount_ui": str(order_output),
1155
- "minimum_allowed_output_amount_ui": str(minimum_allowed),
1156
- "allowed_drift_bps": HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS,
1157
- },
1158
- )
1159
- warnings: list[str] = []
1160
- if order_output < expected_output:
1161
- warnings.append(
1162
- "Houdini order output drifted slightly below the approved preview but stayed within the allowed tolerance."
1163
- )
1164
- return {
1165
- "validated": True,
1166
- "approved_estimated_output_amount_ui": str(expected_output),
1167
- "order_output_amount_ui": str(order_output),
1168
- "minimum_allowed_output_amount_ui": str(minimum_allowed),
1169
- "allowed_drift_bps": HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS,
1170
- "warnings": warnings,
1171
- }
1172
-
1173
- async def _send_houdini_exact_spl_deposit(
1174
- self,
1175
- *,
1176
- recipient_token_account: str,
1177
- mint: str,
1178
- amount_raw: int,
1179
- decimals: int,
1180
- ) -> dict[str, Any]:
1181
- if not self.signer:
1182
- raise WalletBackendError("Solana signer is not configured.")
1183
-
1184
- sender = await self.get_address()
1185
- if not sender:
1186
- raise WalletBackendError(
1187
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
1188
- )
1189
-
1190
- recipient_token_account = validate_solana_address(recipient_token_account)
1191
- mint = validate_solana_mint(mint)
1192
- if amount_raw <= 0:
1193
- raise WalletBackendError("Houdini SPL deposit amount must be greater than zero.")
1194
-
1195
- try:
1196
- from solders.hash import Hash
1197
- from solders.keypair import Keypair
1198
- from solders.message import Message
1199
- from solders.pubkey import Pubkey
1200
- from solders.transaction import Transaction
1201
- from spl.token.instructions import (
1202
- TransferCheckedParams,
1203
- get_associated_token_address,
1204
- transfer_checked,
1205
- )
1206
- except ImportError as exc:
1207
- raise WalletBackendError(
1208
- "solana and solders packages are required for SPL token transfers."
1209
- ) from exc
1210
-
1211
- sender_pubkey = Pubkey.from_string(sender)
1212
- mint_pubkey = Pubkey.from_string(mint)
1213
- token_program_id = await self._resolve_token_program_id(mint)
1214
- token_program_pubkey = Pubkey.from_string(token_program_id)
1215
- sender_token_account = str(
1216
- get_associated_token_address(
1217
- sender_pubkey,
1218
- mint_pubkey,
1219
- token_program_id=token_program_pubkey,
1220
- )
1221
- )
1222
- sender_token_account_exists = await solana_rpc.account_exists(
1223
- sender_token_account,
1224
- rpc_url=self.rpc_urls,
1225
- )
1226
- if not sender_token_account_exists:
1227
- raise WalletBackendError("Sender token account does not exist for this mint.")
1228
- original_deposit_address = recipient_token_account
1229
- await self._wait_for_houdini_spl_deposit_ready(
1230
- deposit_address=recipient_token_account,
1231
- mint=mint,
1232
- )
1233
-
1234
- sender_balance = await solana_rpc.fetch_token_account_balance(
1235
- sender_token_account,
1236
- rpc_url=self.rpc_urls,
1237
- )
1238
- sender_raw_balance = int(sender_balance.get("amount") or 0)
1239
- if amount_raw > sender_raw_balance:
1240
- raise WalletBackendError("Insufficient token balance for this private transfer.")
1241
-
1242
- latest_blockhash = await solana_rpc.fetch_latest_blockhash(
1243
- rpc_url=self.rpc_urls,
1244
- commitment=self.commitment,
1245
- )
1246
- blockhash = Hash.from_string(str(latest_blockhash["blockhash"]))
1247
- keypair = Keypair.from_bytes(self.signer.export_keypair_bytes())
1248
- message = Message.new_with_blockhash(
1249
- [
1250
- transfer_checked(
1251
- TransferCheckedParams(
1252
- program_id=token_program_pubkey,
1253
- source=Pubkey.from_string(sender_token_account),
1254
- mint=mint_pubkey,
1255
- dest=Pubkey.from_string(recipient_token_account),
1256
- owner=sender_pubkey,
1257
- amount=amount_raw,
1258
- decimals=decimals,
1259
- signers=[],
1260
- )
1261
- )
1262
- ],
1263
- sender_pubkey,
1264
- blockhash,
1265
- )
1266
- transaction = Transaction([keypair], message, blockhash)
1267
- submitted = await solana_rpc.send_transaction(
1268
- transaction_base64=encode_transaction_base64(bytes(transaction)),
1269
- rpc_url=self.rpc_urls,
1270
- )
1271
- signature = submitted.get("signature")
1272
- status = None
1273
- confirmed = False
1274
- if isinstance(signature, str) and signature:
1275
- status = await solana_rpc.wait_for_confirmation(
1276
- signature=signature,
1277
- rpc_url=self.rpc_urls,
1278
- )
1279
- confirmed = status is not None
1280
-
1281
- return {
1282
- "chain": "solana",
1283
- "network": self.network,
1284
- "mode": "execute",
1285
- "asset_type": "spl",
1286
- "from_address": sender,
1287
- "to_address": recipient_token_account,
1288
- "requested_deposit_address": original_deposit_address,
1289
- "deposit_address_interpretation": "token_account",
1290
- "mint": mint,
1291
- "token_program_id": token_program_id,
1292
- "sender_token_account": sender_token_account,
1293
- "recipient_token_account": recipient_token_account,
1294
- "recipient_token_account_exists_before": True,
1295
- "recipient_token_account_created": False,
1296
- "amount_ui": float(Decimal(amount_raw) / (Decimal(10) ** decimals)),
1297
- "amount_raw": amount_raw,
1298
- "decimals": decimals,
1299
- "signature": signature,
1300
- "broadcasted": bool(signature),
1301
- "confirmed": confirmed,
1302
- "confirmation_status": status.get("confirmationStatus") if status else None,
1303
- "slot": status.get("slot") if status else None,
1304
- "sign_only": self.sign_only,
1305
- "source": "solana-rpc",
1306
- "execute_response": submitted,
1307
- }
1308
-
1309
- async def execute_solana_private_swap(
1310
- self,
1311
- *,
1312
- input_token: str,
1313
- output_token: str,
1314
- destination_address: str,
1315
- amount_ui: float,
1316
- use_xmr: bool = False,
1317
- approved_preview: dict[str, Any] | None = None,
1318
- existing_order: dict[str, Any] | None = None,
1319
- ) -> dict[str, Any]:
1320
- if self.network != "mainnet":
1321
- raise WalletBackendError("Houdini private Solana swaps are only enabled for Solana mainnet.")
1322
- if not self.signer:
1323
- raise WalletBackendError("Solana signer is not configured.")
1324
- if self.sign_only:
1325
- raise WalletBackendError(
1326
- "This wallet backend is in sign-only mode. Disable sign_only to broadcast transactions."
1327
- )
1328
-
1329
- preview = (
1330
- dict(approved_preview)
1331
- if isinstance(approved_preview, dict)
1332
- else await self.preview_solana_private_swap(
1333
- input_token=input_token,
1334
- output_token=output_token,
1335
- destination_address=destination_address,
1336
- amount_ui=amount_ui,
1337
- use_xmr=use_xmr,
1338
- )
1339
- )
1340
- owner = str(preview.get("owner") or await self.get_address() or "").strip()
1341
- if not owner:
1342
- raise WalletBackendError("No Solana wallet address configured for Houdini execution.")
1343
-
1344
- quote_id = str(preview.get("quote_id") or "").strip()
1345
- if not quote_id:
1346
- raise WalletBackendError("Approved private swap preview is missing quote_id.")
1347
-
1348
- if isinstance(existing_order, dict) and existing_order:
1349
- create_payload = dict(existing_order)
1350
- order = (
1351
- create_payload.get("order")
1352
- if isinstance(create_payload.get("order"), dict)
1353
- else create_payload
1354
- )
1355
- if not isinstance(order, dict):
1356
- raise WalletBackendError("Stored Houdini private swap order is invalid.")
1357
- multi_id = str(create_payload.get("multi_id") or create_payload.get("multiId") or order.get("multiId") or "").strip() or None
1358
- houdini_id = str(
1359
- create_payload.get("houdini_id")
1360
- or create_payload.get("houdiniId")
1361
- or order.get("houdiniId")
1362
- or ""
1363
- ).strip()
1364
- if not houdini_id:
1365
- raise WalletBackendError("Stored Houdini private swap order is missing houdini_id.")
1366
- latest_order = await houdini.fetch_order_status(houdini_id=houdini_id)
1367
- if isinstance(latest_order, dict) and latest_order:
1368
- order = latest_order
1369
-
1370
- order_validation = self._validate_houdini_order_against_preview(
1371
- order=order,
1372
- preview=preview,
1373
- )
1374
- output_validation = self._validate_houdini_order_output_against_preview(
1375
- order=order,
1376
- preview=preview,
1377
- )
1378
- input_decimals = int(preview.get("input_token_decimals") or (9 if bool(preview.get("input_is_native")) else 0))
1379
- input_amount_raw = int(
1380
- (Decimal(str(preview["input_amount_ui"])) * (Decimal(10) ** input_decimals))
1381
- .to_integral_value()
1382
- )
1383
- deposit_address = str(order.get("depositAddress") or "").strip()
1384
- if not deposit_address:
1385
- raise WalletBackendError("Houdini private swap response is missing depositAddress.")
1386
-
1387
- if bool(preview.get("input_is_native")):
1388
- funding_result = await self.send_native_transfer(
1389
- recipient=deposit_address,
1390
- amount_native=float(Decimal(str(preview["input_amount_ui"]))),
1391
- )
1392
- else:
1393
- try:
1394
- funding_result = await self._send_houdini_exact_spl_deposit(
1395
- recipient_token_account=deposit_address,
1396
- mint=str(preview.get("input_token_address") or "").strip(),
1397
- amount_raw=input_amount_raw,
1398
- decimals=input_decimals,
1399
- )
1400
- except WalletBackendError as exc:
1401
- if exc.code == "houdini_deposit_not_ready":
1402
- details = dict(exc.details or {})
1403
- details.update(
1404
- {
1405
- "multi_id": multi_id,
1406
- "houdini_id": houdini_id,
1407
- "deposit_address": deposit_address,
1408
- "order_status": order.get("statusLabel"),
1409
- "order": order,
1410
- "input_token_address": str(preview.get("input_token_address") or "").strip(),
1411
- "input_amount_raw": str(input_amount_raw),
1412
- "input_decimals": input_decimals,
1413
- }
1414
- )
1415
- raise WalletBackendError(
1416
- "Houdini order exists, but its Solana deposit account is not ready yet. Retry continue for the existing order instead of generating a new preview.",
1417
- code="houdini_deposit_not_ready",
1418
- details=details,
1419
- ) from exc
1420
- raise
1421
-
1422
- signature = str(funding_result.get("signature") or "").strip() or None
1423
- confirmed = bool(funding_result.get("confirmed"))
1424
-
1425
- return {
1426
- "chain": "solana",
1427
- "network": self.network,
1428
- "mode": "execute",
1429
- "asset_type": "solana-private-swap",
1430
- "owner": owner,
1431
- "destination_address": str(preview["destination_address"]),
1432
- "input_token_id": preview["input_token_id"],
1433
- "output_token_id": preview["output_token_id"],
1434
- "input_token_symbol": preview["input_token_symbol"],
1435
- "output_token_symbol": preview["output_token_symbol"],
1436
- "input_token_address": preview["input_token_address"],
1437
- "output_token_address": preview["output_token_address"],
1438
- "input_is_native": bool(preview.get("input_is_native")),
1439
- "input_amount_ui": preview["input_amount_ui"],
1440
- "estimated_output_amount_ui": preview["estimated_output_amount_ui"],
1441
- "private_duration_minutes": order.get("eta") or preview.get("private_duration_minutes"),
1442
- "multi_id": multi_id,
1443
- "houdini_id": houdini_id,
1444
- "deposit_address": deposit_address,
1445
- "order_status": order.get("statusLabel"),
1446
- "order": order,
1447
- "signature": signature,
1448
- "broadcasted": bool(signature),
1449
- "confirmed": confirmed,
1450
- "confirmation_status": funding_result.get("confirmation_status"),
1451
- "slot": funding_result.get("slot"),
1452
- "verification": {
1453
- "verified": True,
1454
- "deposit_address": deposit_address,
1455
- "amount_raw": str(input_amount_raw),
1456
- "is_native_input": bool(preview.get("input_is_native")),
1457
- "token_mint": (
1458
- None
1459
- if bool(preview.get("input_is_native"))
1460
- else str(preview.get("input_token_address") or "").strip()
1461
- ),
1462
- "quote_bound_single_exchange": True,
1463
- },
1464
- "simulation": None,
1465
- "provider_order_validation": order_validation,
1466
- "output_validation": output_validation,
1467
- "funding_transfer": funding_result,
1468
- "execute_response": funding_result.get("execute_response"),
1469
- "status_tracking": {
1470
- "multi_id": multi_id,
1471
- "houdini_id": houdini_id,
1472
- "poll_status_tool": "get_solana_private_swap_status",
1473
- },
1474
- "source": "houdini",
1475
- "execution_state": "funding_submitted",
1476
- }
1477
- else:
1478
- try:
1479
- create_payload = await houdini.create_exchange(
1480
- quote_id=quote_id,
1481
- destination_address=str(preview["destination_address"]),
1482
- )
1483
- except ProviderError as exc:
1484
- details = dict(exc.details or {})
1485
- gateway_error = details.get("error") if isinstance(details.get("error"), dict) else None
1486
- if isinstance(gateway_error, dict) and str(gateway_error.get("code") or "").strip() == "RATE_LIMIT_EXCEEDED":
1487
- retry_after = gateway_error.get("retryAfter")
1488
- raise WalletBackendError(
1489
- "Houdini exchange create is rate-limited right now. Wait and retry execute without generating a new preview.",
1490
- code="houdini_exchange_rate_limited",
1491
- details={
1492
- "retry_after": retry_after,
1493
- "quote_id": quote_id,
1494
- "destination_address": str(preview["destination_address"]),
1495
- "provider": getattr(exc, "provider", "houdini"),
1496
- "upstream_error": gateway_error,
1497
- },
1498
- ) from exc
1499
- raise
1500
- order = create_payload.get("order") if isinstance(create_payload.get("order"), dict) else create_payload
1501
- if not isinstance(order, dict):
1502
- raise WalletBackendError("Houdini returned no order object for the private swap.")
1503
- if isinstance(order.get("error"), dict):
1504
- error = order["error"]
1505
- raise WalletBackendError(
1506
- f"Houdini rejected the private swap request: {error.get('message') or 'unknown error'}.",
1507
- code=str(error.get("code") or "").strip() or None,
1508
- details=error,
1509
- )
1510
-
1511
- multi_id = str(create_payload.get("multiId") or order.get("multiId") or "").strip() or None
1512
- houdini_id = str(order.get("houdiniId") or create_payload.get("houdiniId") or "").strip()
1513
- if not houdini_id:
1514
- raise WalletBackendError("Houdini private swap response is missing the order identifier.")
1515
- deposit_address = str(order.get("depositAddress") or "").strip()
1516
- if not deposit_address:
1517
- raise WalletBackendError("Houdini private swap response is missing depositAddress.")
1518
-
1519
- order_validation = self._validate_houdini_order_against_preview(
1520
- order=order,
1521
- preview=preview,
1522
- )
1523
- output_validation = self._validate_houdini_order_output_against_preview(
1524
- order=order,
1525
- preview=preview,
1526
- )
1527
-
1528
- return {
1529
- "chain": "solana",
1530
- "network": self.network,
1531
- "mode": "execute",
1532
- "asset_type": "solana-private-swap",
1533
- "owner": owner,
1534
- "destination_address": str(preview["destination_address"]),
1535
- "input_token_id": preview["input_token_id"],
1536
- "output_token_id": preview["output_token_id"],
1537
- "input_token_symbol": preview["input_token_symbol"],
1538
- "output_token_symbol": preview["output_token_symbol"],
1539
- "input_token_address": preview["input_token_address"],
1540
- "output_token_address": preview["output_token_address"],
1541
- "input_is_native": bool(preview.get("input_is_native")),
1542
- "input_amount_ui": preview["input_amount_ui"],
1543
- "estimated_output_amount_ui": preview["estimated_output_amount_ui"],
1544
- "private_duration_minutes": order.get("eta") or preview.get("private_duration_minutes"),
1545
- "multi_id": multi_id,
1546
- "houdini_id": houdini_id,
1547
- "deposit_address": deposit_address,
1548
- "order_status": order.get("statusLabel"),
1549
- "order": order,
1550
- "provider_order_validation": order_validation,
1551
- "output_validation": output_validation,
1552
- "status_tracking": {
1553
- "multi_id": multi_id,
1554
- "houdini_id": houdini_id,
1555
- "poll_status_tool": "get_solana_private_swap_status",
1556
- },
1557
- "source": "houdini",
1558
- "execution_state": "awaiting_deposit_funding",
1559
- "next_step": "Call continue_solana_private_swap with the same approved private swap context to submit the funding transfer.",
1560
- }
1561
-
1562
- async def continue_solana_private_swap(
1563
- self,
1564
- *,
1565
- approved_preview: dict[str, Any],
1566
- existing_order: dict[str, Any],
1567
- ) -> dict[str, Any]:
1568
- return await self.execute_solana_private_swap(
1569
- input_token=str(approved_preview.get("input_token_query") or approved_preview.get("input_token_symbol") or ""),
1570
- output_token=str(approved_preview.get("output_token_query") or approved_preview.get("output_token_symbol") or ""),
1571
- destination_address=str(approved_preview.get("destination_address") or ""),
1572
- amount_ui=float(approved_preview.get("input_amount_ui") or 0),
1573
- use_xmr=bool(approved_preview.get("use_xmr", False)),
1574
- approved_preview=approved_preview,
1575
- existing_order=existing_order,
1576
- )
1577
-
1578
723
  async def preview_solana_lifi_cross_chain_swap(
1579
724
  self,
1580
725
  *,
@@ -2422,109 +1567,39 @@ class SolanaWalletBackend(AgentWalletBackend):
2422
1567
  if not isinstance(platform, str) or not platform.strip():
2423
1568
  raise WalletBackendError("Each platform must be a non-empty string.")
2424
1569
  platform_filter.append(platform.strip())
2425
- data = await jupiter.fetch_portfolio_positions(
2426
- address=wallet_address,
2427
- platforms=platform_filter,
2428
- )
2429
- positions = data.get("positions")
2430
- if not isinstance(positions, list):
2431
- positions = data.get("data") if isinstance(data.get("data"), list) else []
2432
- return {
2433
- "chain": "solana",
2434
- "network": self.network,
2435
- "address": wallet_address,
2436
- "platforms": platform_filter or [],
2437
- "position_count": len(positions),
2438
- "positions": positions,
2439
- "raw": data,
2440
- "source": "jupiter-portfolio",
2441
- }
2442
-
2443
- async def get_jupiter_staked_jup(self, address: str | None = None) -> dict[str, Any]:
2444
- self._require_mainnet_jupiter("Jupiter staked JUP")
2445
- wallet_address = address or self.address
2446
- if not wallet_address:
2447
- raise WalletBackendError(
2448
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
2449
- )
2450
- wallet_address = validate_solana_address(wallet_address)
2451
- data = await jupiter.fetch_staked_jup(address=wallet_address)
2452
- return {
2453
- "chain": "solana",
2454
- "network": self.network,
2455
- "address": wallet_address,
2456
- "raw": data,
2457
- "source": "jupiter-portfolio",
2458
- }
2459
-
2460
- async def get_jupiter_earn_tokens(self) -> dict[str, Any]:
2461
- self._require_mainnet_jupiter("Jupiter Earn")
2462
- data = await jupiter.fetch_earn_tokens()
2463
- tokens = data.get("tokens")
2464
- if not isinstance(tokens, list):
2465
- tokens = []
2466
- return {
2467
- "chain": "solana",
2468
- "network": self.network,
2469
- "token_count": len(tokens),
2470
- "tokens": tokens,
2471
- "raw": data,
2472
- "source": "jupiter-lend",
2473
- }
2474
-
2475
- async def get_jupiter_earn_positions(
2476
- self,
2477
- users: list[str] | None = None,
2478
- ) -> dict[str, Any]:
2479
- self._require_mainnet_jupiter("Jupiter Earn")
2480
- resolved_users = users or [self.address]
2481
- if not resolved_users or any(user is None for user in resolved_users):
2482
- raise WalletBackendError("At least one wallet address is required for Earn positions.")
2483
- normalized_users = [validate_solana_address(str(user)) for user in resolved_users]
2484
- data = await jupiter.fetch_earn_positions(users=normalized_users)
1570
+ data = await jupiter.fetch_portfolio_positions(
1571
+ address=wallet_address,
1572
+ platforms=platform_filter,
1573
+ )
2485
1574
  positions = data.get("positions")
2486
1575
  if not isinstance(positions, list):
2487
- positions = []
1576
+ positions = data.get("data") if isinstance(data.get("data"), list) else []
2488
1577
  return {
2489
1578
  "chain": "solana",
2490
1579
  "network": self.network,
2491
- "users": normalized_users,
1580
+ "address": wallet_address,
1581
+ "platforms": platform_filter or [],
2492
1582
  "position_count": len(positions),
2493
1583
  "positions": positions,
2494
1584
  "raw": data,
2495
- "source": "jupiter-lend",
1585
+ "source": "jupiter-portfolio",
2496
1586
  }
2497
1587
 
2498
- async def get_jupiter_earn_earnings(
2499
- self,
2500
- user: str | None = None,
2501
- positions: list[str] | None = None,
2502
- ) -> dict[str, Any]:
2503
- self._require_mainnet_jupiter("Jupiter Earn")
2504
- wallet_address = user or self.address
1588
+ async def get_jupiter_staked_jup(self, address: str | None = None) -> dict[str, Any]:
1589
+ self._require_mainnet_jupiter("Jupiter staked JUP")
1590
+ wallet_address = address or self.address
2505
1591
  if not wallet_address:
2506
1592
  raise WalletBackendError(
2507
- "A wallet address is required for Jupiter Earn earnings lookup."
1593
+ "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
2508
1594
  )
2509
- if not positions:
2510
- raise WalletBackendError("positions must include at least one Earn position address.")
2511
1595
  wallet_address = validate_solana_address(wallet_address)
2512
- normalized_positions = [validate_solana_address(str(position)) for position in positions]
2513
- data = await jupiter.fetch_earn_earnings(
2514
- user=wallet_address,
2515
- positions=normalized_positions,
2516
- )
2517
- earnings = data.get("earnings")
2518
- if not isinstance(earnings, list):
2519
- earnings = []
1596
+ data = await jupiter.fetch_staked_jup(address=wallet_address)
2520
1597
  return {
2521
1598
  "chain": "solana",
2522
1599
  "network": self.network,
2523
- "user": wallet_address,
2524
- "positions": normalized_positions,
2525
- "earnings": earnings,
1600
+ "address": wallet_address,
2526
1601
  "raw": data,
2527
- "source": "jupiter-lend",
1602
+ "source": "jupiter-portfolio",
2528
1603
  }
2529
1604
 
2530
1605
  async def get_flash_trade_markets(
@@ -3217,6 +2292,440 @@ class SolanaWalletBackend(AgentWalletBackend):
3217
2292
  "source": "kamino",
3218
2293
  }
3219
2294
 
2295
+ async def get_kamino_open_positions(self, user: str | None = None) -> dict[str, Any]:
2296
+ self._require_mainnet_kamino("Kamino lending")
2297
+ wallet_address = user or self.address
2298
+ if not wallet_address:
2299
+ raise WalletBackendError("A wallet address is required for Kamino position lookup.")
2300
+ wallet_address = validate_solana_address(wallet_address)
2301
+
2302
+ markets_snapshot = await self.get_kamino_lend_markets()
2303
+ markets = markets_snapshot.get("markets")
2304
+ if not isinstance(markets, list):
2305
+ markets = []
2306
+
2307
+ lookup_errors: list[dict[str, Any]] = []
2308
+ semaphore = asyncio.Semaphore(KAMINO_OPEN_POSITIONS_SCAN_CONCURRENCY)
2309
+
2310
+ def _market_address(entry: Any) -> str:
2311
+ return _kamino_entry_address(entry, "lendingMarket", "market", "address")
2312
+
2313
+ def _market_name(entry: Any) -> str | None:
2314
+ if isinstance(entry, dict):
2315
+ value = entry.get("name")
2316
+ if isinstance(value, str) and value.strip():
2317
+ return value.strip()
2318
+ return None
2319
+
2320
+ async def _fetch_market_obligations(
2321
+ market_entry: dict[str, Any],
2322
+ ) -> tuple[dict[str, Any], dict[str, Any]] | None:
2323
+ market_address = _market_address(market_entry)
2324
+ if not market_address:
2325
+ return None
2326
+ try:
2327
+ async with semaphore:
2328
+ obligations_snapshot = await self.get_kamino_lend_user_obligations(
2329
+ market=market_address,
2330
+ user=wallet_address,
2331
+ )
2332
+ except (ProviderError, WalletBackendError) as exc:
2333
+ lookup_errors.append(
2334
+ {
2335
+ "stage": "market_obligations",
2336
+ "market": market_address,
2337
+ "market_name": _market_name(market_entry),
2338
+ "error": str(exc),
2339
+ }
2340
+ )
2341
+ return None
2342
+ if int(obligations_snapshot.get("obligation_count") or 0) <= 0:
2343
+ return None
2344
+ return market_entry, obligations_snapshot
2345
+
2346
+ market_results = await asyncio.gather(
2347
+ *[
2348
+ _fetch_market_obligations(market_entry)
2349
+ for market_entry in markets
2350
+ if isinstance(market_entry, dict)
2351
+ ]
2352
+ )
2353
+ active_markets = [result for result in market_results if result is not None]
2354
+ discovered_obligation_count = sum(
2355
+ int(obligations_snapshot.get("obligation_count") or 0)
2356
+ for _, obligations_snapshot in active_markets
2357
+ )
2358
+
2359
+ try:
2360
+ reward_snapshot = await self.get_kamino_lend_user_rewards(user=wallet_address)
2361
+ except (ProviderError, WalletBackendError) as exc:
2362
+ lookup_errors.append(
2363
+ {
2364
+ "stage": "rewards",
2365
+ "user": wallet_address,
2366
+ "error": str(exc),
2367
+ }
2368
+ )
2369
+ reward_snapshot = {
2370
+ "chain": "solana",
2371
+ "network": self.network,
2372
+ "user": wallet_address,
2373
+ "reward_count": 0,
2374
+ "rewards": [],
2375
+ "avg_base_apy": None,
2376
+ "avg_boosted_apy": None,
2377
+ "avg_max_apy": None,
2378
+ "source": "kamino",
2379
+ }
2380
+ reward_items = reward_snapshot.get("rewards")
2381
+ if not isinstance(reward_items, list):
2382
+ reward_items = []
2383
+
2384
+ positions: list[dict[str, Any]] = []
2385
+ markets_with_positions: list[dict[str, Any]] = []
2386
+ total_collateral_value = Decimal("0")
2387
+ total_borrow_value = Decimal("0")
2388
+
2389
+ for market_entry, obligations_snapshot in active_markets:
2390
+ market_address = _market_address(market_entry)
2391
+ market_name = _market_name(market_entry)
2392
+ market_description = (
2393
+ market_entry.get("description")
2394
+ if isinstance(market_entry, dict) and isinstance(market_entry.get("description"), str)
2395
+ else None
2396
+ )
2397
+ markets_with_positions.append(
2398
+ {
2399
+ "market": market_address,
2400
+ "market_name": market_name,
2401
+ "obligation_count": int(obligations_snapshot.get("obligation_count") or 0),
2402
+ }
2403
+ )
2404
+
2405
+ try:
2406
+ reserve_snapshot = await self.get_kamino_lend_market_reserves(market=market_address)
2407
+ except (ProviderError, WalletBackendError) as exc:
2408
+ lookup_errors.append(
2409
+ {
2410
+ "stage": "market_reserves",
2411
+ "market": market_address,
2412
+ "market_name": market_name,
2413
+ "error": str(exc),
2414
+ }
2415
+ )
2416
+ reserve_snapshot = {
2417
+ "chain": "solana",
2418
+ "network": self.network,
2419
+ "market": market_address,
2420
+ "reserve_count": 0,
2421
+ "reserves": [],
2422
+ "source": "kamino",
2423
+ }
2424
+ reserves = reserve_snapshot.get("reserves")
2425
+ if not isinstance(reserves, list):
2426
+ reserves = []
2427
+ reserve_by_address = {
2428
+ address: reserve
2429
+ for reserve in reserves
2430
+ if isinstance(reserve, dict)
2431
+ and (address := _kamino_entry_address(reserve, "reserve"))
2432
+ }
2433
+ reserve_by_mint = {
2434
+ mint: reserve
2435
+ for reserve in reserves
2436
+ if isinstance(reserve, dict)
2437
+ and isinstance((mint := reserve.get("liquidityTokenMint")), str)
2438
+ and mint.strip()
2439
+ }
2440
+ reserve_by_symbol = {
2441
+ symbol.upper(): reserve
2442
+ for reserve in reserves
2443
+ if isinstance(reserve, dict)
2444
+ and isinstance((symbol := reserve.get("liquidityToken")), str)
2445
+ and symbol.strip()
2446
+ }
2447
+
2448
+ def _reward_metrics_for_reserve(
2449
+ *,
2450
+ reserve_address: str | None,
2451
+ side: str,
2452
+ ) -> list[dict[str, Any]]:
2453
+ if not reserve_address:
2454
+ return []
2455
+ reserve_key = "depositReserve" if side == "deposit" else "borrowReserve"
2456
+ metrics: list[dict[str, Any]] = []
2457
+ for reward in reward_items:
2458
+ if not isinstance(reward, dict):
2459
+ continue
2460
+ reward_market = _kamino_entry_address(reward, "market")
2461
+ if reward_market and reward_market != market_address:
2462
+ continue
2463
+ reward_reserve = _kamino_entry_address(reward, reserve_key)
2464
+ if reward_reserve != reserve_address:
2465
+ continue
2466
+ metrics.append(
2467
+ {
2468
+ "reward_mint": _kamino_entry_address(reward, "rewardMint", "rewardToken"),
2469
+ "tokens_earned": reward.get("tokensEarned"),
2470
+ "tokens_per_second": reward.get("tokensPerSecond"),
2471
+ "base_apy": reward.get("baseApy"),
2472
+ "boosted_apy": reward.get("boostedApy"),
2473
+ "max_apy": reward.get("maxApy"),
2474
+ "usd_amount": reward.get("usdAmount"),
2475
+ "usd_amount_boosted": reward.get("usdAmountBoosted"),
2476
+ "staking_boost": reward.get("stakingBoost"),
2477
+ "effective_staking_boost": reward.get("effectiveStakingBoost"),
2478
+ "last_calculated": reward.get("lastCalculated"),
2479
+ }
2480
+ )
2481
+ return metrics
2482
+
2483
+ obligations = obligations_snapshot.get("obligations")
2484
+ if not isinstance(obligations, list):
2485
+ obligations = []
2486
+ for obligation in obligations:
2487
+ if not isinstance(obligation, dict):
2488
+ continue
2489
+ obligation_address = _kamino_entry_address(
2490
+ obligation,
2491
+ "obligationAddress",
2492
+ "loanId",
2493
+ "address",
2494
+ )
2495
+ if not obligation_address:
2496
+ continue
2497
+ try:
2498
+ loan_data = await kamino.fetch_lend_loan_info(
2499
+ obligation=obligation_address,
2500
+ network=self.network,
2501
+ )
2502
+ except ProviderError as exc:
2503
+ lookup_errors.append(
2504
+ {
2505
+ "stage": "loan_info",
2506
+ "market": market_address,
2507
+ "market_name": market_name,
2508
+ "obligation_address": obligation_address,
2509
+ "error": str(exc),
2510
+ }
2511
+ )
2512
+ continue
2513
+
2514
+ loan_info = loan_data.get("loanInfo")
2515
+ if not isinstance(loan_info, dict):
2516
+ loan_info = {}
2517
+ collateral = loan_info.get("collateral")
2518
+ if not isinstance(collateral, dict):
2519
+ collateral = {}
2520
+ debt = loan_info.get("debt")
2521
+ if not isinstance(debt, dict):
2522
+ debt = {}
2523
+ deposit_entries = collateral.get("deposits")
2524
+ if not isinstance(deposit_entries, list):
2525
+ deposit_entries = []
2526
+ borrow_entries = debt.get("borrows")
2527
+ if not isinstance(borrow_entries, list):
2528
+ borrow_entries = []
2529
+
2530
+ state = obligation.get("state")
2531
+ if not isinstance(state, dict):
2532
+ state = {}
2533
+ state_deposits = [
2534
+ entry
2535
+ for entry in state.get("deposits", [])
2536
+ if isinstance(entry, dict)
2537
+ and (_coerce_decimal(entry.get("depositedAmount")) or Decimal("0")) > 0
2538
+ ]
2539
+ state_borrows = [
2540
+ entry
2541
+ for entry in state.get("borrows", [])
2542
+ if isinstance(entry, dict)
2543
+ and (
2544
+ (_coerce_decimal(entry.get("borrowedAmountSf")) or Decimal("0")) > 0
2545
+ or (_coerce_decimal(entry.get("marketValueSf")) or Decimal("0")) > 0
2546
+ )
2547
+ ]
2548
+
2549
+ def _match_reserve(
2550
+ *,
2551
+ token_mint: str | None,
2552
+ token_name: str | None,
2553
+ fallback_entry: Any,
2554
+ reserve_key: str,
2555
+ ) -> tuple[str | None, dict[str, Any] | None]:
2556
+ fallback_address = _kamino_entry_address(fallback_entry, reserve_key)
2557
+ if fallback_address and fallback_address in reserve_by_address:
2558
+ return fallback_address, reserve_by_address[fallback_address]
2559
+ if token_mint and token_mint in reserve_by_mint:
2560
+ reserve_entry = reserve_by_mint[token_mint]
2561
+ return _kamino_entry_address(reserve_entry, "reserve") or None, reserve_entry
2562
+ symbol = token_name.strip().upper() if isinstance(token_name, str) and token_name.strip() else None
2563
+ if symbol and symbol in reserve_by_symbol:
2564
+ reserve_entry = reserve_by_symbol[symbol]
2565
+ return _kamino_entry_address(reserve_entry, "reserve") or None, reserve_entry
2566
+ return fallback_address or None, None
2567
+
2568
+ def _enrich_position_entries(
2569
+ *,
2570
+ entries: list[dict[str, Any]],
2571
+ state_entries: list[dict[str, Any]],
2572
+ side: str,
2573
+ ) -> list[dict[str, Any]]:
2574
+ enriched: list[dict[str, Any]] = []
2575
+ reserve_key = "depositReserve" if side == "deposit" else "borrowReserve"
2576
+ for index, entry in enumerate(entries):
2577
+ if not isinstance(entry, dict):
2578
+ continue
2579
+ token_mint = entry.get("tokenMint")
2580
+ token_name = entry.get("tokenName")
2581
+ fallback_entry = state_entries[index] if index < len(state_entries) else None
2582
+ reserve_address, reserve_metrics = _match_reserve(
2583
+ token_mint=token_mint if isinstance(token_mint, str) else None,
2584
+ token_name=token_name if isinstance(token_name, str) else None,
2585
+ fallback_entry=fallback_entry,
2586
+ reserve_key=reserve_key,
2587
+ )
2588
+ reward_metrics = _reward_metrics_for_reserve(
2589
+ reserve_address=reserve_address,
2590
+ side=side,
2591
+ )
2592
+ enriched.append(
2593
+ {
2594
+ "reserve": reserve_address,
2595
+ "token_mint": token_mint,
2596
+ "token_name": token_name,
2597
+ "token_amount": entry.get("tokenAmount"),
2598
+ "token_value_usd": entry.get("tokenValue"),
2599
+ "token_price_usd": entry.get("tokenPrice"),
2600
+ "max_ltv": entry.get("maxLtv"),
2601
+ "liquidation_ltv": entry.get("liquidationLtv"),
2602
+ "max_withdrawable_amount": entry.get("maxWithdrawableAmount"),
2603
+ "max_withdrawable_value_usd": entry.get("maxWithdrawableValue"),
2604
+ "max_borrowable_amount": entry.get("maxBorrowableAmount"),
2605
+ "max_borrowable_value_usd": entry.get("maxBorrowableValue"),
2606
+ "borrow_factor": entry.get("borrowFactor"),
2607
+ "reserve_supply_apy": (
2608
+ reserve_metrics.get("supplyApy")
2609
+ if isinstance(reserve_metrics, dict)
2610
+ else None
2611
+ ),
2612
+ "reserve_borrow_apy": (
2613
+ reserve_metrics.get("borrowApy")
2614
+ if isinstance(reserve_metrics, dict)
2615
+ else None
2616
+ ),
2617
+ "reserve_max_ltv": (
2618
+ reserve_metrics.get("maxLtv")
2619
+ if isinstance(reserve_metrics, dict)
2620
+ else None
2621
+ ),
2622
+ "reward_metrics": reward_metrics,
2623
+ "reward_count": len(reward_metrics),
2624
+ }
2625
+ )
2626
+ return enriched
2627
+
2628
+ enriched_deposits = _enrich_position_entries(
2629
+ entries=deposit_entries,
2630
+ state_entries=state_deposits,
2631
+ side="deposit",
2632
+ )
2633
+ enriched_borrows = _enrich_position_entries(
2634
+ entries=borrow_entries,
2635
+ state_entries=state_borrows,
2636
+ side="borrow",
2637
+ )
2638
+
2639
+ collateral_value = sum(
2640
+ (
2641
+ _coerce_decimal(entry.get("token_value_usd")) or Decimal("0")
2642
+ for entry in enriched_deposits
2643
+ ),
2644
+ Decimal("0"),
2645
+ )
2646
+ borrow_value = sum(
2647
+ (
2648
+ _coerce_decimal(entry.get("token_value_usd")) or Decimal("0")
2649
+ for entry in enriched_borrows
2650
+ ),
2651
+ Decimal("0"),
2652
+ )
2653
+ total_collateral_value += collateral_value
2654
+ total_borrow_value += borrow_value
2655
+ refreshed_stats = obligation.get("refreshedStats")
2656
+ if not isinstance(refreshed_stats, dict):
2657
+ refreshed_stats = {}
2658
+ position_type = "borrow-lend"
2659
+ if enriched_deposits and not enriched_borrows:
2660
+ position_type = "lend"
2661
+ elif enriched_borrows and not enriched_deposits:
2662
+ position_type = "borrow"
2663
+
2664
+ positions.append(
2665
+ {
2666
+ "obligation_address": obligation_address,
2667
+ "market": market_address,
2668
+ "market_name": market_name,
2669
+ "market_description": market_description,
2670
+ "user": wallet_address,
2671
+ "position_type": position_type,
2672
+ "has_debt": bool(enriched_borrows),
2673
+ "timestamp": loan_data.get("timestamp"),
2674
+ "solana_slot": loan_data.get("solanaSlot"),
2675
+ "elevation_group": loan_data.get("elevationGroup"),
2676
+ "leverage": loan_data.get("leverage"),
2677
+ "collateral_value_usd": _format_decimal(collateral_value),
2678
+ "borrow_value_usd": _format_decimal(borrow_value),
2679
+ "net_value_usd": _format_decimal(collateral_value - borrow_value),
2680
+ "loan_info": {
2681
+ "current_ltv": loan_info.get("currentLtv"),
2682
+ "max_ltv": loan_info.get("maxLtv"),
2683
+ "liquidation_ltv": loan_info.get("liquidationLtv"),
2684
+ "close_factor": loan_info.get("closeFactor"),
2685
+ "collateral": {
2686
+ "deposit_count": len(enriched_deposits),
2687
+ "total_value_usd": _format_decimal(collateral_value),
2688
+ "deposits": enriched_deposits,
2689
+ },
2690
+ "debt": {
2691
+ "borrow_count": len(enriched_borrows),
2692
+ "total_value_usd": _format_decimal(borrow_value),
2693
+ "borrows": enriched_borrows,
2694
+ },
2695
+ },
2696
+ "refreshed_stats": {
2697
+ "borrow_limit": refreshed_stats.get("borrowLimit"),
2698
+ "borrow_liquidation_limit": refreshed_stats.get("borrowLiquidationLimit"),
2699
+ "borrow_utilization": refreshed_stats.get("borrowUtilization"),
2700
+ "net_account_value": refreshed_stats.get("netAccountValue"),
2701
+ },
2702
+ "source": "kamino+klend-loans",
2703
+ }
2704
+ )
2705
+
2706
+ return {
2707
+ "chain": "solana",
2708
+ "network": self.network,
2709
+ "user": wallet_address,
2710
+ "market_count_scanned": len(markets),
2711
+ "markets_with_positions_count": len(markets_with_positions),
2712
+ "markets_with_positions": markets_with_positions,
2713
+ "discovered_obligation_count": discovered_obligation_count,
2714
+ "position_count": len(positions),
2715
+ "positions": positions,
2716
+ "total_collateral_value_usd": _format_decimal(total_collateral_value),
2717
+ "total_borrow_value_usd": _format_decimal(total_borrow_value),
2718
+ "total_net_value_usd": _format_decimal(total_collateral_value - total_borrow_value),
2719
+ "reward_summary": {
2720
+ "reward_count": int(reward_snapshot.get("reward_count") or 0),
2721
+ "avg_base_apy": reward_snapshot.get("avg_base_apy"),
2722
+ "avg_boosted_apy": reward_snapshot.get("avg_boosted_apy"),
2723
+ "avg_max_apy": reward_snapshot.get("avg_max_apy"),
2724
+ },
2725
+ "lookup_errors": lookup_errors,
2726
+ "source": "kamino+klend-loans",
2727
+ }
2728
+
3220
2729
  async def get_state(self) -> SolanaWalletState:
3221
2730
  balance_native = None
3222
2731
  if self.address:
@@ -3790,53 +3299,6 @@ class SolanaWalletBackend(AgentWalletBackend):
3790
3299
  )
3791
3300
  return encode_transaction_base64(bytes(signed_transaction))
3792
3301
 
3793
- async def _prepare_jupiter_lend_transaction(
3794
- self,
3795
- *,
3796
- transaction_base64: str,
3797
- action: str,
3798
- asset: str,
3799
- amount_raw: str,
3800
- ) -> dict[str, Any]:
3801
- if not self.signer:
3802
- raise WalletBackendError("Solana signer is not configured.")
3803
- try:
3804
- from solders.transaction import VersionedTransaction
3805
- except ImportError as exc:
3806
- raise WalletBackendError(
3807
- "solana and solders packages are required for Jupiter Earn transaction signing."
3808
- ) from exc
3809
- unsigned_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_base64))
3810
- owner = await self.get_address()
3811
- verification = verify_provider_lend_transaction(
3812
- unsigned_transaction.message,
3813
- wallet_address=str(owner),
3814
- asset_mint=asset,
3815
- action=f"Jupiter Earn {action}",
3816
- )
3817
- signed_transaction_base64 = await self._sign_versioned_provider_transaction(
3818
- transaction_base64=transaction_base64,
3819
- wallet_signer_index=int(verification.get("wallet_signer_index") or 0),
3820
- )
3821
- return {
3822
- "chain": "solana",
3823
- "network": self.network,
3824
- "mode": "prepare",
3825
- "asset_type": f"jupiter-earn-{action}",
3826
- "owner": owner,
3827
- "asset": asset,
3828
- "amount_raw": amount_raw,
3829
- "transaction_base64": signed_transaction_base64,
3830
- "transaction_encoding": "base64",
3831
- "transaction_format": "versioned",
3832
- "signed": True,
3833
- "broadcasted": False,
3834
- "confirmed": False,
3835
- "verification": verification,
3836
- "sign_only": self.sign_only,
3837
- "source": "jupiter-lend",
3838
- }
3839
-
3840
3302
  async def _execute_prepared_provider_transaction(
3841
3303
  self,
3842
3304
  prepared: dict[str, Any],
@@ -3883,12 +3345,6 @@ class SolanaWalletBackend(AgentWalletBackend):
3883
3345
  "kamino_safety": prepared.get("kamino_safety"),
3884
3346
  }
3885
3347
 
3886
- async def _execute_prepared_jupiter_lend_transaction(self, prepared: dict[str, Any]) -> dict[str, Any]:
3887
- return await self._execute_prepared_provider_transaction(
3888
- prepared,
3889
- source="jupiter-lend",
3890
- )
3891
-
3892
3348
  def _find_kamino_reserve_entry(
3893
3349
  self,
3894
3350
  *,
@@ -4529,143 +3985,6 @@ class SolanaWalletBackend(AgentWalletBackend):
4529
3985
  result["build_response"] = prepared.get("build_response")
4530
3986
  return result
4531
3987
 
4532
- async def preview_jupiter_earn_deposit(
4533
- self,
4534
- asset: str,
4535
- amount_raw: str,
4536
- ) -> dict[str, Any]:
4537
- self._require_mainnet_jupiter("Jupiter Earn")
4538
- owner = await self.get_address()
4539
- if not owner:
4540
- raise WalletBackendError(
4541
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
4542
- )
4543
- amount_raw = _require_positive_integer_string(amount_raw, field_name="amount_raw")
4544
- asset = validate_solana_mint(asset)
4545
- tokens = await self.get_jupiter_earn_tokens()
4546
- token_entry = next(
4547
- (
4548
- item
4549
- for item in tokens["tokens"]
4550
- if isinstance(item, dict)
4551
- and str(item.get("asset") or item.get("mint") or "").strip() == asset
4552
- ),
4553
- None,
4554
- )
4555
- if token_entry is None:
4556
- raise WalletBackendError("Requested asset is not currently available in Jupiter Earn.")
4557
- return {
4558
- "chain": "solana",
4559
- "network": self.network,
4560
- "mode": "preview",
4561
- "asset_type": "jupiter-earn-deposit",
4562
- "owner": owner,
4563
- "asset": asset,
4564
- "amount_raw": amount_raw,
4565
- "token": token_entry,
4566
- "sign_only": self.sign_only,
4567
- "can_send": self.get_capabilities().can_send_transaction,
4568
- "source": "jupiter-lend",
4569
- }
4570
-
4571
- async def prepare_jupiter_earn_deposit(
4572
- self,
4573
- asset: str,
4574
- amount_raw: str,
4575
- ) -> dict[str, Any]:
4576
- preview = await self.preview_jupiter_earn_deposit(asset=asset, amount_raw=amount_raw)
4577
- owner = str(preview["owner"])
4578
- build = await jupiter.build_earn_deposit_transaction(
4579
- asset=str(preview["asset"]),
4580
- user_address=owner,
4581
- amount_raw=str(preview["amount_raw"]),
4582
- )
4583
- prepared = await self._prepare_jupiter_lend_transaction(
4584
- transaction_base64=str(build["transaction"]),
4585
- action="deposit",
4586
- asset=str(preview["asset"]),
4587
- amount_raw=str(preview["amount_raw"]),
4588
- )
4589
- prepared["build_response"] = build
4590
- return prepared
4591
-
4592
- async def execute_jupiter_earn_deposit(
4593
- self,
4594
- asset: str,
4595
- amount_raw: str,
4596
- ) -> dict[str, Any]:
4597
- prepared = await self.prepare_jupiter_earn_deposit(asset=asset, amount_raw=amount_raw)
4598
- result = await self._execute_prepared_jupiter_lend_transaction(prepared)
4599
- result["build_response"] = prepared.get("build_response")
4600
- return result
4601
-
4602
- async def preview_jupiter_earn_withdraw(
4603
- self,
4604
- asset: str,
4605
- amount_raw: str,
4606
- ) -> dict[str, Any]:
4607
- self._require_mainnet_jupiter("Jupiter Earn")
4608
- owner = await self.get_address()
4609
- if not owner:
4610
- raise WalletBackendError(
4611
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
4612
- )
4613
- amount_raw = _require_positive_integer_string(amount_raw, field_name="amount_raw")
4614
- asset = validate_solana_mint(asset)
4615
- positions = await self.get_jupiter_earn_positions(users=[owner])
4616
- matching_positions = [
4617
- item
4618
- for item in positions["positions"]
4619
- if isinstance(item, dict)
4620
- and str(item.get("asset") or item.get("mint") or "").strip() == asset
4621
- ]
4622
- if not matching_positions:
4623
- raise WalletBackendError("No Jupiter Earn position found for the requested asset.")
4624
- return {
4625
- "chain": "solana",
4626
- "network": self.network,
4627
- "mode": "preview",
4628
- "asset_type": "jupiter-earn-withdraw",
4629
- "owner": owner,
4630
- "asset": asset,
4631
- "amount_raw": amount_raw,
4632
- "positions": matching_positions,
4633
- "sign_only": self.sign_only,
4634
- "can_send": self.get_capabilities().can_send_transaction,
4635
- "source": "jupiter-lend",
4636
- }
4637
-
4638
- async def prepare_jupiter_earn_withdraw(
4639
- self,
4640
- asset: str,
4641
- amount_raw: str,
4642
- ) -> dict[str, Any]:
4643
- preview = await self.preview_jupiter_earn_withdraw(asset=asset, amount_raw=amount_raw)
4644
- owner = str(preview["owner"])
4645
- build = await jupiter.build_earn_withdraw_transaction(
4646
- asset=str(preview["asset"]),
4647
- user_address=owner,
4648
- amount_raw=str(preview["amount_raw"]),
4649
- )
4650
- prepared = await self._prepare_jupiter_lend_transaction(
4651
- transaction_base64=str(build["transaction"]),
4652
- action="withdraw",
4653
- asset=str(preview["asset"]),
4654
- amount_raw=str(preview["amount_raw"]),
4655
- )
4656
- prepared["build_response"] = build
4657
- return prepared
4658
-
4659
- async def execute_jupiter_earn_withdraw(
4660
- self,
4661
- asset: str,
4662
- amount_raw: str,
4663
- ) -> dict[str, Any]:
4664
- prepared = await self.prepare_jupiter_earn_withdraw(asset=asset, amount_raw=amount_raw)
4665
- result = await self._execute_prepared_jupiter_lend_transaction(prepared)
4666
- result["build_response"] = prepared.get("build_response")
4667
- return result
4668
-
4669
3988
  async def preview_native_transfer(
4670
3989
  self,
4671
3990
  recipient: str,
@@ -4809,49 +4128,6 @@ class SolanaWalletBackend(AgentWalletBackend):
4809
4128
  "source": "solana-rpc",
4810
4129
  }
4811
4130
 
4812
- async def request_testnet_airdrop(self, amount_native: float) -> dict[str, Any]:
4813
- if self.network not in {"devnet", "testnet"}:
4814
- raise WalletBackendError("Airdrop is only available on Solana devnet or testnet.")
4815
- if amount_native <= 0:
4816
- raise WalletBackendError("amount must be greater than zero.")
4817
-
4818
- address = await self.get_address()
4819
- if not address:
4820
- raise WalletBackendError(
4821
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
4822
- )
4823
-
4824
- lamports = int(round(amount_native * solana_rpc.LAMPORTS_PER_SOL))
4825
- submitted = await solana_rpc.request_airdrop(
4826
- address=address,
4827
- lamports=lamports,
4828
- rpc_url=self.rpc_urls,
4829
- commitment=self.commitment,
4830
- )
4831
- signature = submitted.get("signature")
4832
- status = None
4833
- confirmed = False
4834
- if isinstance(signature, str) and signature:
4835
- status = await solana_rpc.wait_for_confirmation(
4836
- signature=signature,
4837
- rpc_url=self.rpc_urls,
4838
- )
4839
- confirmed = status is not None
4840
-
4841
- return {
4842
- "chain": "solana",
4843
- "network": self.network,
4844
- "mode": "airdrop",
4845
- "address": address,
4846
- "amount_native": amount_native,
4847
- "amount_lamports": lamports,
4848
- "signature": signature,
4849
- "confirmed": confirmed,
4850
- "confirmation_status": status.get("confirmationStatus") if status else None,
4851
- "slot": status.get("slot") if status else None,
4852
- "source": "solana-rpc",
4853
- }
4854
-
4855
4131
  async def _resolve_mint_decimals(self, mint: str) -> int:
4856
4132
  if mint == NATIVE_SOL_MINT:
4857
4133
  return 9