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