@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.
- package/.openclaw/extensions/agent-wallet/README.md +1 -2
- package/.openclaw/extensions/agent-wallet/dist/index.js +6 -340
- package/.openclaw/extensions/agent-wallet/index.ts +6 -340
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +0 -43
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +1 -3
- package/CHANGELOG.md +35 -0
- package/README.md +0 -5
- package/agent-wallet/.env.example +0 -12
- package/agent-wallet/README.md +0 -35
- package/agent-wallet/agent_wallet/btc_user_wallets.py +32 -1
- package/agent-wallet/agent_wallet/config.py +11 -7
- package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +1 -655
- package/agent-wallet/agent_wallet/openclaw_cli.py +0 -7
- package/agent-wallet/agent_wallet/providers/evm_portfolio.py +18 -42
- package/agent-wallet/agent_wallet/providers/jupiter.py +1 -307
- package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +31 -3
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +37 -3
- package/agent-wallet/agent_wallet/transaction_policy.py +0 -262
- package/agent-wallet/agent_wallet/wallet_layer/base.py +0 -100
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +1 -1118
- package/agent-wallet/openclaw.plugin.json +0 -4
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/install_agent_wallet.py +113 -6
- package/agent-wallet/scripts/install_openclaw_local_config.py +7 -5
- package/agent-wallet/skills/wallet-operator/SKILL.md +1 -5
- package/bin/openclaw-agent-wallet.mjs +103 -36
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +7 -2
- package/codex/plugins/agent-wallet/server.py +2 -118
- package/hermes/plugins/agent_wallet/tools.py +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/src/local_vault.js +45 -68
- package/wdk-btc-wallet/src/server.js +1 -0
- package/wdk-evm-wallet/README.md +4 -3
- package/wdk-evm-wallet/src/config.js +15 -0
- package/wdk-evm-wallet/src/local_vault.js +45 -68
- package/wdk-evm-wallet/src/server.js +1 -0
- 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,
|
|
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,
|