@agentlayer.tech/wallet 0.1.25 → 0.1.27
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 -1
- package/.openclaw/extensions/agent-wallet/dist/index.js +35 -9
- package/.openclaw/extensions/agent-wallet/index.ts +35 -9
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +4 -0
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +5 -1
- package/CHANGELOG.md +26 -0
- package/agent-wallet/README.md +1 -1
- package/agent-wallet/agent_wallet/config.py +1 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +242 -15
- package/agent-wallet/agent_wallet/openclaw_cli.py +1 -0
- package/agent-wallet/agent_wallet/providers/jupiter.py +127 -14
- package/agent-wallet/agent_wallet/providers/kamino.py +6 -0
- package/agent-wallet/agent_wallet/transaction_policy.py +6 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +131 -5
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +528 -43
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +7 -2
- package/package.json +2 -2
|
@@ -7,6 +7,7 @@ import base64
|
|
|
7
7
|
import binascii
|
|
8
8
|
import hashlib
|
|
9
9
|
import json
|
|
10
|
+
import time
|
|
10
11
|
from decimal import Decimal, InvalidOperation
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
@@ -48,6 +49,8 @@ TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
|
48
49
|
NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112"
|
|
49
50
|
STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111"
|
|
50
51
|
HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS = 600
|
|
52
|
+
SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS = 300
|
|
53
|
+
SOLANA_SWAP_INTENT_DEFAULT_MAX_FEE_LAMPORTS = 6_000_000
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
def _load_signing_key():
|
|
@@ -2062,6 +2065,29 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
2062
2065
|
parts.append(f"route fee {route_fee_bps} bps (already reflected in quoted output)")
|
|
2063
2066
|
return "; ".join(parts)
|
|
2064
2067
|
|
|
2068
|
+
def _swap_fee_lamports(self, payload: dict[str, Any]) -> int | None:
|
|
2069
|
+
fee_summary = payload.get("fee_summary")
|
|
2070
|
+
if isinstance(fee_summary, dict):
|
|
2071
|
+
network_fee = _coerce_int(fee_summary.get("network_fee_lamports"))
|
|
2072
|
+
if network_fee is not None:
|
|
2073
|
+
return network_fee
|
|
2074
|
+
return None
|
|
2075
|
+
|
|
2076
|
+
def _default_swap_intent_max_fee_lamports(self, fee_summary: dict[str, Any]) -> int:
|
|
2077
|
+
estimated_fee = _coerce_int(fee_summary.get("network_fee_lamports")) or 0
|
|
2078
|
+
return max(
|
|
2079
|
+
estimated_fee * 3,
|
|
2080
|
+
estimated_fee + 100_000,
|
|
2081
|
+
SOLANA_SWAP_INTENT_DEFAULT_MAX_FEE_LAMPORTS,
|
|
2082
|
+
)
|
|
2083
|
+
|
|
2084
|
+
def _swap_minimum_output_floor(self, *, out_amount_raw: int, slippage_bps: int) -> int:
|
|
2085
|
+
if out_amount_raw <= 0:
|
|
2086
|
+
return 0
|
|
2087
|
+
if slippage_bps <= 0:
|
|
2088
|
+
raise WalletBackendError("slippage_bps must be greater than zero.")
|
|
2089
|
+
return max(1, (out_amount_raw * max(0, 10_000 - slippage_bps)) // 10_000)
|
|
2090
|
+
|
|
2065
2091
|
def _require_mainnet_bags(self, feature: str) -> None:
|
|
2066
2092
|
if self.network != "mainnet":
|
|
2067
2093
|
raise WalletBackendError(f"{feature} is only enabled for Solana mainnet.")
|
|
@@ -3739,19 +3765,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3739
3765
|
keypair = Keypair.from_bytes(self.signer.export_keypair_bytes())
|
|
3740
3766
|
try:
|
|
3741
3767
|
unsigned_transaction = VersionedTransaction.from_bytes(raw_transaction)
|
|
3742
|
-
|
|
3743
|
-
signatures = list(unsigned_transaction.signatures)
|
|
3744
|
-
if wallet_signer_index >= len(signatures):
|
|
3745
|
-
raise WalletBackendError(
|
|
3746
|
-
"Provider transaction signer layout is incompatible with local signing."
|
|
3747
|
-
)
|
|
3748
|
-
signatures[wallet_signer_index] = signature
|
|
3749
|
-
signed_transaction = VersionedTransaction.populate(
|
|
3750
|
-
unsigned_transaction.message,
|
|
3751
|
-
signatures,
|
|
3752
|
-
)
|
|
3753
|
-
return encode_transaction_base64(bytes(signed_transaction))
|
|
3754
|
-
except Exception:
|
|
3768
|
+
except (TypeError, ValueError):
|
|
3755
3769
|
unsigned_transaction = Transaction.from_bytes(raw_transaction)
|
|
3756
3770
|
signatures = list(unsigned_transaction.signatures)
|
|
3757
3771
|
if wallet_signer_index >= len(signatures):
|
|
@@ -3763,6 +3777,18 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3763
3777
|
unsigned_transaction.message.recent_blockhash,
|
|
3764
3778
|
)
|
|
3765
3779
|
return encode_transaction_base64(bytes(unsigned_transaction))
|
|
3780
|
+
signature = keypair.sign_message(to_bytes_versioned(unsigned_transaction.message))
|
|
3781
|
+
signatures = list(unsigned_transaction.signatures)
|
|
3782
|
+
if wallet_signer_index >= len(signatures):
|
|
3783
|
+
raise WalletBackendError(
|
|
3784
|
+
"Provider transaction signer layout is incompatible with local signing."
|
|
3785
|
+
)
|
|
3786
|
+
signatures[wallet_signer_index] = signature
|
|
3787
|
+
signed_transaction = VersionedTransaction.populate(
|
|
3788
|
+
unsigned_transaction.message,
|
|
3789
|
+
signatures,
|
|
3790
|
+
)
|
|
3791
|
+
return encode_transaction_base64(bytes(signed_transaction))
|
|
3766
3792
|
|
|
3767
3793
|
async def _prepare_jupiter_lend_transaction(
|
|
3768
3794
|
self,
|
|
@@ -3821,9 +3847,11 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3821
3847
|
raise WalletBackendError(
|
|
3822
3848
|
"This wallet backend is in sign-only mode. Disable sign_only to broadcast transactions."
|
|
3823
3849
|
)
|
|
3850
|
+
kamino_verified = bool((prepared.get("kamino_safety") or {}).get("verified"))
|
|
3824
3851
|
submitted = await solana_rpc.send_transaction(
|
|
3825
3852
|
transaction_base64=str(prepared["transaction_base64"]),
|
|
3826
3853
|
rpc_url=self.rpc_urls,
|
|
3854
|
+
skip_preflight=source == "kamino" and kamino_verified,
|
|
3827
3855
|
)
|
|
3828
3856
|
signature = submitted.get("signature")
|
|
3829
3857
|
status = None
|
|
@@ -3832,6 +3860,8 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3832
3860
|
status = await solana_rpc.wait_for_confirmation(
|
|
3833
3861
|
signature=signature,
|
|
3834
3862
|
rpc_url=self.rpc_urls,
|
|
3863
|
+
timeout_seconds=60.0 if source == "kamino" else 20.0,
|
|
3864
|
+
poll_interval_seconds=2.0 if source == "kamino" else 1.0,
|
|
3835
3865
|
)
|
|
3836
3866
|
confirmed = status is not None
|
|
3837
3867
|
return {
|
|
@@ -3849,6 +3879,8 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3849
3879
|
"slot": status.get("slot") if status else None,
|
|
3850
3880
|
"sign_only": self.sign_only,
|
|
3851
3881
|
"source": source,
|
|
3882
|
+
"simulation": prepared.get("simulation"),
|
|
3883
|
+
"kamino_safety": prepared.get("kamino_safety"),
|
|
3852
3884
|
}
|
|
3853
3885
|
|
|
3854
3886
|
async def _execute_prepared_jupiter_lend_transaction(self, prepared: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -3899,6 +3931,37 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3899
3931
|
matches.append(item)
|
|
3900
3932
|
return matches
|
|
3901
3933
|
|
|
3934
|
+
def _resolve_kamino_obligation_selection(
|
|
3935
|
+
self,
|
|
3936
|
+
*,
|
|
3937
|
+
obligations: list[Any],
|
|
3938
|
+
obligation_address: str | None,
|
|
3939
|
+
action: str,
|
|
3940
|
+
) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
|
|
3941
|
+
candidates = [item for item in obligations if isinstance(item, dict)]
|
|
3942
|
+
requested = str(obligation_address or "").strip()
|
|
3943
|
+
if requested:
|
|
3944
|
+
requested = validate_solana_address(requested)
|
|
3945
|
+
for item in candidates:
|
|
3946
|
+
if (
|
|
3947
|
+
_kamino_entry_address(
|
|
3948
|
+
item,
|
|
3949
|
+
"obligationAddress",
|
|
3950
|
+
"obligation",
|
|
3951
|
+
"address",
|
|
3952
|
+
"pubkey",
|
|
3953
|
+
"loanId",
|
|
3954
|
+
)
|
|
3955
|
+
== requested
|
|
3956
|
+
):
|
|
3957
|
+
return candidates, item
|
|
3958
|
+
raise WalletBackendError(
|
|
3959
|
+
f"Requested obligation_address is not available for Kamino {action} in the selected market."
|
|
3960
|
+
)
|
|
3961
|
+
if len(candidates) == 1:
|
|
3962
|
+
return candidates, candidates[0]
|
|
3963
|
+
return candidates, None
|
|
3964
|
+
|
|
3902
3965
|
async def _prepare_kamino_lend_transaction(
|
|
3903
3966
|
self,
|
|
3904
3967
|
*,
|
|
@@ -3907,6 +3970,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3907
3970
|
market: str,
|
|
3908
3971
|
reserve: str,
|
|
3909
3972
|
amount_ui: str,
|
|
3973
|
+
obligation_address: str | None = None,
|
|
3910
3974
|
) -> dict[str, Any]:
|
|
3911
3975
|
if not self.signer:
|
|
3912
3976
|
raise WalletBackendError("Solana signer is not configured.")
|
|
@@ -3927,12 +3991,49 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3927
3991
|
market_address=market,
|
|
3928
3992
|
reserve_address=reserve,
|
|
3929
3993
|
action=f"Kamino {action}",
|
|
3994
|
+
obligation_address=obligation_address,
|
|
3930
3995
|
loaded_addresses=loaded_addresses,
|
|
3931
3996
|
)
|
|
3932
3997
|
signed_transaction_base64 = await self._sign_versioned_provider_transaction(
|
|
3933
3998
|
transaction_base64=transaction_base64,
|
|
3934
3999
|
wallet_signer_index=int(verification.get("wallet_signer_index") or 0),
|
|
3935
4000
|
)
|
|
4001
|
+
simulation_value: dict[str, Any] | None = None
|
|
4002
|
+
kamino_safety: dict[str, Any]
|
|
4003
|
+
try:
|
|
4004
|
+
simulation = await solana_rpc.simulate_transaction(
|
|
4005
|
+
transaction_base64=signed_transaction_base64,
|
|
4006
|
+
rpc_url=self.rpc_urls,
|
|
4007
|
+
commitment=self.commitment,
|
|
4008
|
+
)
|
|
4009
|
+
simulation_value = (
|
|
4010
|
+
simulation.get("value") if isinstance(simulation.get("value"), dict) else {}
|
|
4011
|
+
)
|
|
4012
|
+
if isinstance(simulation_value, dict) and simulation_value.get("err") is not None:
|
|
4013
|
+
raise WalletBackendError(
|
|
4014
|
+
f"Kamino {action} transaction simulation failed.",
|
|
4015
|
+
code="kamino_simulation_failed",
|
|
4016
|
+
details={
|
|
4017
|
+
"simulation": simulation_value,
|
|
4018
|
+
"action": action,
|
|
4019
|
+
"market": market,
|
|
4020
|
+
"reserve": reserve,
|
|
4021
|
+
},
|
|
4022
|
+
)
|
|
4023
|
+
kamino_safety = {
|
|
4024
|
+
"verified": True,
|
|
4025
|
+
"simulation_unavailable": False,
|
|
4026
|
+
}
|
|
4027
|
+
except ProviderError as exc:
|
|
4028
|
+
kamino_safety = {
|
|
4029
|
+
"verified": False,
|
|
4030
|
+
"simulation_unavailable": True,
|
|
4031
|
+
"warning": (
|
|
4032
|
+
"Kamino simulation could not be completed via the configured Solana RPC. "
|
|
4033
|
+
"Proceeding with structural provider verification only."
|
|
4034
|
+
),
|
|
4035
|
+
"error": str(exc),
|
|
4036
|
+
}
|
|
3936
4037
|
return {
|
|
3937
4038
|
"chain": "solana",
|
|
3938
4039
|
"network": self.network,
|
|
@@ -3941,6 +4042,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3941
4042
|
"owner": owner,
|
|
3942
4043
|
"market": market,
|
|
3943
4044
|
"reserve": reserve,
|
|
4045
|
+
"obligation_address": obligation_address,
|
|
3944
4046
|
"amount_ui": amount_ui,
|
|
3945
4047
|
"transaction_base64": signed_transaction_base64,
|
|
3946
4048
|
"transaction_encoding": "base64",
|
|
@@ -3949,15 +4051,30 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3949
4051
|
"broadcasted": False,
|
|
3950
4052
|
"confirmed": False,
|
|
3951
4053
|
"verification": verification,
|
|
4054
|
+
"simulation": simulation_value,
|
|
4055
|
+
"kamino_safety": kamino_safety,
|
|
3952
4056
|
"sign_only": self.sign_only,
|
|
3953
4057
|
"source": "kamino",
|
|
3954
4058
|
}
|
|
3955
4059
|
|
|
4060
|
+
def _kamino_preview_from_approved(
|
|
4061
|
+
self,
|
|
4062
|
+
approved_preview: dict[str, Any] | None,
|
|
4063
|
+
*,
|
|
4064
|
+
asset_type: str,
|
|
4065
|
+
) -> dict[str, Any] | None:
|
|
4066
|
+
if not isinstance(approved_preview, dict):
|
|
4067
|
+
return None
|
|
4068
|
+
if str(approved_preview.get("asset_type") or "").strip() != asset_type:
|
|
4069
|
+
return None
|
|
4070
|
+
return dict(approved_preview)
|
|
4071
|
+
|
|
3956
4072
|
async def preview_kamino_lend_deposit(
|
|
3957
4073
|
self,
|
|
3958
4074
|
market: str,
|
|
3959
4075
|
reserve: str,
|
|
3960
4076
|
amount_ui: str,
|
|
4077
|
+
obligation_address: str | None = None,
|
|
3961
4078
|
) -> dict[str, Any]:
|
|
3962
4079
|
self._require_mainnet_kamino("Kamino lending")
|
|
3963
4080
|
owner = await self.get_address()
|
|
@@ -3995,8 +4112,13 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3995
4112
|
market: str,
|
|
3996
4113
|
reserve: str,
|
|
3997
4114
|
amount_ui: str,
|
|
4115
|
+
obligation_address: str | None = None,
|
|
4116
|
+
approved_preview: dict[str, Any] | None = None,
|
|
3998
4117
|
) -> dict[str, Any]:
|
|
3999
|
-
preview =
|
|
4118
|
+
preview = self._kamino_preview_from_approved(
|
|
4119
|
+
approved_preview,
|
|
4120
|
+
asset_type="kamino-lend-deposit",
|
|
4121
|
+
) or await self.preview_kamino_lend_deposit(
|
|
4000
4122
|
market=market,
|
|
4001
4123
|
reserve=reserve,
|
|
4002
4124
|
amount_ui=amount_ui,
|
|
@@ -4023,11 +4145,14 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4023
4145
|
market: str,
|
|
4024
4146
|
reserve: str,
|
|
4025
4147
|
amount_ui: str,
|
|
4148
|
+
obligation_address: str | None = None,
|
|
4149
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4026
4150
|
) -> dict[str, Any]:
|
|
4027
4151
|
prepared = await self.prepare_kamino_lend_deposit(
|
|
4028
4152
|
market=market,
|
|
4029
4153
|
reserve=reserve,
|
|
4030
4154
|
amount_ui=amount_ui,
|
|
4155
|
+
approved_preview=approved_preview,
|
|
4031
4156
|
)
|
|
4032
4157
|
result = await self._execute_prepared_provider_transaction(prepared, source="kamino")
|
|
4033
4158
|
result["build_response"] = prepared.get("build_response")
|
|
@@ -4038,6 +4163,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4038
4163
|
market: str,
|
|
4039
4164
|
reserve: str,
|
|
4040
4165
|
amount_ui: str,
|
|
4166
|
+
obligation_address: str | None = None,
|
|
4041
4167
|
) -> dict[str, Any]:
|
|
4042
4168
|
self._require_mainnet_kamino("Kamino lending")
|
|
4043
4169
|
owner = await self.get_address()
|
|
@@ -4062,6 +4188,19 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4062
4188
|
)
|
|
4063
4189
|
if not obligation_matches:
|
|
4064
4190
|
raise WalletBackendError("No Kamino obligation found for the requested reserve.")
|
|
4191
|
+
obligation_options, selected_obligation = self._resolve_kamino_obligation_selection(
|
|
4192
|
+
obligations=obligation_matches,
|
|
4193
|
+
obligation_address=obligation_address,
|
|
4194
|
+
action="withdraw",
|
|
4195
|
+
)
|
|
4196
|
+
selected_obligation_address = _kamino_entry_address(
|
|
4197
|
+
selected_obligation,
|
|
4198
|
+
"obligationAddress",
|
|
4199
|
+
"obligation",
|
|
4200
|
+
"address",
|
|
4201
|
+
"pubkey",
|
|
4202
|
+
"loanId",
|
|
4203
|
+
)
|
|
4065
4204
|
return {
|
|
4066
4205
|
"chain": "solana",
|
|
4067
4206
|
"network": self.network,
|
|
@@ -4072,7 +4211,14 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4072
4211
|
"reserve": reserve,
|
|
4073
4212
|
"amount_ui": amount_ui,
|
|
4074
4213
|
"reserve_info": reserve_entry,
|
|
4075
|
-
"obligations":
|
|
4214
|
+
"obligations": obligation_options,
|
|
4215
|
+
"obligation_options": [
|
|
4216
|
+
_kamino_entry_address(item, "obligationAddress", "obligation", "address", "pubkey", "loanId")
|
|
4217
|
+
for item in obligation_options
|
|
4218
|
+
if _kamino_entry_address(item, "obligationAddress", "obligation", "address", "pubkey", "loanId")
|
|
4219
|
+
],
|
|
4220
|
+
"obligation_address": selected_obligation_address or None,
|
|
4221
|
+
"requires_obligation_address": selected_obligation is None and len(obligation_options) > 1,
|
|
4076
4222
|
"sign_only": self.sign_only,
|
|
4077
4223
|
"can_send": self.get_capabilities().can_send_transaction,
|
|
4078
4224
|
"source": "kamino",
|
|
@@ -4083,12 +4229,23 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4083
4229
|
market: str,
|
|
4084
4230
|
reserve: str,
|
|
4085
4231
|
amount_ui: str,
|
|
4232
|
+
obligation_address: str | None = None,
|
|
4233
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4086
4234
|
) -> dict[str, Any]:
|
|
4087
|
-
preview =
|
|
4235
|
+
preview = self._kamino_preview_from_approved(
|
|
4236
|
+
approved_preview,
|
|
4237
|
+
asset_type="kamino-lend-withdraw",
|
|
4238
|
+
) or await self.preview_kamino_lend_withdraw(
|
|
4088
4239
|
market=market,
|
|
4089
4240
|
reserve=reserve,
|
|
4090
4241
|
amount_ui=amount_ui,
|
|
4242
|
+
obligation_address=obligation_address,
|
|
4091
4243
|
)
|
|
4244
|
+
selected_obligation_address = str(preview.get("obligation_address") or "").strip()
|
|
4245
|
+
if bool(preview.get("requires_obligation_address")) and not selected_obligation_address:
|
|
4246
|
+
raise WalletBackendError(
|
|
4247
|
+
"Kamino withdraw requires obligation_address when multiple obligations match the selected market/reserve."
|
|
4248
|
+
)
|
|
4092
4249
|
owner = str(preview["owner"])
|
|
4093
4250
|
build = await kamino.build_lend_withdraw_transaction(
|
|
4094
4251
|
wallet=owner,
|
|
@@ -4102,6 +4259,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4102
4259
|
market=str(preview["market"]),
|
|
4103
4260
|
reserve=str(preview["reserve"]),
|
|
4104
4261
|
amount_ui=str(preview["amount_ui"]),
|
|
4262
|
+
obligation_address=selected_obligation_address or None,
|
|
4105
4263
|
)
|
|
4106
4264
|
prepared["build_response"] = build
|
|
4107
4265
|
return prepared
|
|
@@ -4111,11 +4269,15 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4111
4269
|
market: str,
|
|
4112
4270
|
reserve: str,
|
|
4113
4271
|
amount_ui: str,
|
|
4272
|
+
obligation_address: str | None = None,
|
|
4273
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4114
4274
|
) -> dict[str, Any]:
|
|
4115
4275
|
prepared = await self.prepare_kamino_lend_withdraw(
|
|
4116
4276
|
market=market,
|
|
4117
4277
|
reserve=reserve,
|
|
4118
4278
|
amount_ui=amount_ui,
|
|
4279
|
+
obligation_address=obligation_address,
|
|
4280
|
+
approved_preview=approved_preview,
|
|
4119
4281
|
)
|
|
4120
4282
|
result = await self._execute_prepared_provider_transaction(prepared, source="kamino")
|
|
4121
4283
|
result["build_response"] = prepared.get("build_response")
|
|
@@ -4126,6 +4288,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4126
4288
|
market: str,
|
|
4127
4289
|
reserve: str,
|
|
4128
4290
|
amount_ui: str,
|
|
4291
|
+
obligation_address: str | None = None,
|
|
4129
4292
|
) -> dict[str, Any]:
|
|
4130
4293
|
self._require_mainnet_kamino("Kamino lending")
|
|
4131
4294
|
owner = await self.get_address()
|
|
@@ -4146,6 +4309,19 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4146
4309
|
obligations = await self.get_kamino_lend_user_obligations(market=market, user=owner)
|
|
4147
4310
|
if int(obligations["obligation_count"]) <= 0:
|
|
4148
4311
|
raise WalletBackendError("Kamino borrow requires an existing obligation in the selected market.")
|
|
4312
|
+
obligation_options, selected_obligation = self._resolve_kamino_obligation_selection(
|
|
4313
|
+
obligations=list(obligations["obligations"]),
|
|
4314
|
+
obligation_address=obligation_address,
|
|
4315
|
+
action="borrow",
|
|
4316
|
+
)
|
|
4317
|
+
selected_obligation_address = _kamino_entry_address(
|
|
4318
|
+
selected_obligation,
|
|
4319
|
+
"obligationAddress",
|
|
4320
|
+
"obligation",
|
|
4321
|
+
"address",
|
|
4322
|
+
"pubkey",
|
|
4323
|
+
"loanId",
|
|
4324
|
+
)
|
|
4149
4325
|
return {
|
|
4150
4326
|
"chain": "solana",
|
|
4151
4327
|
"network": self.network,
|
|
@@ -4156,7 +4332,14 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4156
4332
|
"reserve": reserve,
|
|
4157
4333
|
"amount_ui": amount_ui,
|
|
4158
4334
|
"reserve_info": reserve_entry,
|
|
4159
|
-
"obligations":
|
|
4335
|
+
"obligations": obligation_options,
|
|
4336
|
+
"obligation_options": [
|
|
4337
|
+
_kamino_entry_address(item, "obligationAddress", "obligation", "address", "pubkey", "loanId")
|
|
4338
|
+
for item in obligation_options
|
|
4339
|
+
if _kamino_entry_address(item, "obligationAddress", "obligation", "address", "pubkey", "loanId")
|
|
4340
|
+
],
|
|
4341
|
+
"obligation_address": selected_obligation_address or None,
|
|
4342
|
+
"requires_obligation_address": selected_obligation is None and len(obligation_options) > 1,
|
|
4160
4343
|
"sign_only": self.sign_only,
|
|
4161
4344
|
"can_send": self.get_capabilities().can_send_transaction,
|
|
4162
4345
|
"source": "kamino",
|
|
@@ -4167,12 +4350,23 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4167
4350
|
market: str,
|
|
4168
4351
|
reserve: str,
|
|
4169
4352
|
amount_ui: str,
|
|
4353
|
+
obligation_address: str | None = None,
|
|
4354
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4170
4355
|
) -> dict[str, Any]:
|
|
4171
|
-
preview =
|
|
4356
|
+
preview = self._kamino_preview_from_approved(
|
|
4357
|
+
approved_preview,
|
|
4358
|
+
asset_type="kamino-lend-borrow",
|
|
4359
|
+
) or await self.preview_kamino_lend_borrow(
|
|
4172
4360
|
market=market,
|
|
4173
4361
|
reserve=reserve,
|
|
4174
4362
|
amount_ui=amount_ui,
|
|
4363
|
+
obligation_address=obligation_address,
|
|
4175
4364
|
)
|
|
4365
|
+
selected_obligation_address = str(preview.get("obligation_address") or "").strip()
|
|
4366
|
+
if bool(preview.get("requires_obligation_address")) and not selected_obligation_address:
|
|
4367
|
+
raise WalletBackendError(
|
|
4368
|
+
"Kamino borrow requires obligation_address when multiple obligations exist in the selected market."
|
|
4369
|
+
)
|
|
4176
4370
|
owner = str(preview["owner"])
|
|
4177
4371
|
build = await kamino.build_lend_borrow_transaction(
|
|
4178
4372
|
wallet=owner,
|
|
@@ -4186,6 +4380,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4186
4380
|
market=str(preview["market"]),
|
|
4187
4381
|
reserve=str(preview["reserve"]),
|
|
4188
4382
|
amount_ui=str(preview["amount_ui"]),
|
|
4383
|
+
obligation_address=selected_obligation_address or None,
|
|
4189
4384
|
)
|
|
4190
4385
|
prepared["build_response"] = build
|
|
4191
4386
|
return prepared
|
|
@@ -4195,11 +4390,15 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4195
4390
|
market: str,
|
|
4196
4391
|
reserve: str,
|
|
4197
4392
|
amount_ui: str,
|
|
4393
|
+
obligation_address: str | None = None,
|
|
4394
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4198
4395
|
) -> dict[str, Any]:
|
|
4199
4396
|
prepared = await self.prepare_kamino_lend_borrow(
|
|
4200
4397
|
market=market,
|
|
4201
4398
|
reserve=reserve,
|
|
4202
4399
|
amount_ui=amount_ui,
|
|
4400
|
+
obligation_address=obligation_address,
|
|
4401
|
+
approved_preview=approved_preview,
|
|
4203
4402
|
)
|
|
4204
4403
|
result = await self._execute_prepared_provider_transaction(prepared, source="kamino")
|
|
4205
4404
|
result["build_response"] = prepared.get("build_response")
|
|
@@ -4210,6 +4409,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4210
4409
|
market: str,
|
|
4211
4410
|
reserve: str,
|
|
4212
4411
|
amount_ui: str,
|
|
4412
|
+
obligation_address: str | None = None,
|
|
4213
4413
|
) -> dict[str, Any]:
|
|
4214
4414
|
self._require_mainnet_kamino("Kamino lending")
|
|
4215
4415
|
owner = await self.get_address()
|
|
@@ -4234,6 +4434,19 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4234
4434
|
)
|
|
4235
4435
|
if not obligation_matches:
|
|
4236
4436
|
raise WalletBackendError("No Kamino debt position found for the requested reserve.")
|
|
4437
|
+
obligation_options, selected_obligation = self._resolve_kamino_obligation_selection(
|
|
4438
|
+
obligations=obligation_matches,
|
|
4439
|
+
obligation_address=obligation_address,
|
|
4440
|
+
action="repay",
|
|
4441
|
+
)
|
|
4442
|
+
selected_obligation_address = _kamino_entry_address(
|
|
4443
|
+
selected_obligation,
|
|
4444
|
+
"obligationAddress",
|
|
4445
|
+
"obligation",
|
|
4446
|
+
"address",
|
|
4447
|
+
"pubkey",
|
|
4448
|
+
"loanId",
|
|
4449
|
+
)
|
|
4237
4450
|
return {
|
|
4238
4451
|
"chain": "solana",
|
|
4239
4452
|
"network": self.network,
|
|
@@ -4244,7 +4457,14 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4244
4457
|
"reserve": reserve,
|
|
4245
4458
|
"amount_ui": amount_ui,
|
|
4246
4459
|
"reserve_info": reserve_entry,
|
|
4247
|
-
"obligations":
|
|
4460
|
+
"obligations": obligation_options,
|
|
4461
|
+
"obligation_options": [
|
|
4462
|
+
_kamino_entry_address(item, "obligationAddress", "obligation", "address", "pubkey", "loanId")
|
|
4463
|
+
for item in obligation_options
|
|
4464
|
+
if _kamino_entry_address(item, "obligationAddress", "obligation", "address", "pubkey", "loanId")
|
|
4465
|
+
],
|
|
4466
|
+
"obligation_address": selected_obligation_address or None,
|
|
4467
|
+
"requires_obligation_address": selected_obligation is None and len(obligation_options) > 1,
|
|
4248
4468
|
"sign_only": self.sign_only,
|
|
4249
4469
|
"can_send": self.get_capabilities().can_send_transaction,
|
|
4250
4470
|
"source": "kamino",
|
|
@@ -4255,12 +4475,23 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4255
4475
|
market: str,
|
|
4256
4476
|
reserve: str,
|
|
4257
4477
|
amount_ui: str,
|
|
4478
|
+
obligation_address: str | None = None,
|
|
4479
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4258
4480
|
) -> dict[str, Any]:
|
|
4259
|
-
preview =
|
|
4481
|
+
preview = self._kamino_preview_from_approved(
|
|
4482
|
+
approved_preview,
|
|
4483
|
+
asset_type="kamino-lend-repay",
|
|
4484
|
+
) or await self.preview_kamino_lend_repay(
|
|
4260
4485
|
market=market,
|
|
4261
4486
|
reserve=reserve,
|
|
4262
4487
|
amount_ui=amount_ui,
|
|
4488
|
+
obligation_address=obligation_address,
|
|
4263
4489
|
)
|
|
4490
|
+
selected_obligation_address = str(preview.get("obligation_address") or "").strip()
|
|
4491
|
+
if bool(preview.get("requires_obligation_address")) and not selected_obligation_address:
|
|
4492
|
+
raise WalletBackendError(
|
|
4493
|
+
"Kamino repay requires obligation_address when multiple debt obligations match the selected market/reserve."
|
|
4494
|
+
)
|
|
4264
4495
|
owner = str(preview["owner"])
|
|
4265
4496
|
build = await kamino.build_lend_repay_transaction(
|
|
4266
4497
|
wallet=owner,
|
|
@@ -4274,6 +4505,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4274
4505
|
market=str(preview["market"]),
|
|
4275
4506
|
reserve=str(preview["reserve"]),
|
|
4276
4507
|
amount_ui=str(preview["amount_ui"]),
|
|
4508
|
+
obligation_address=selected_obligation_address or None,
|
|
4277
4509
|
)
|
|
4278
4510
|
prepared["build_response"] = build
|
|
4279
4511
|
return prepared
|
|
@@ -4283,11 +4515,15 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4283
4515
|
market: str,
|
|
4284
4516
|
reserve: str,
|
|
4285
4517
|
amount_ui: str,
|
|
4518
|
+
obligation_address: str | None = None,
|
|
4519
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4286
4520
|
) -> dict[str, Any]:
|
|
4287
4521
|
prepared = await self.prepare_kamino_lend_repay(
|
|
4288
4522
|
market=market,
|
|
4289
4523
|
reserve=reserve,
|
|
4290
4524
|
amount_ui=amount_ui,
|
|
4525
|
+
obligation_address=obligation_address,
|
|
4526
|
+
approved_preview=approved_preview,
|
|
4291
4527
|
)
|
|
4292
4528
|
result = await self._execute_prepared_provider_transaction(prepared, source="kamino")
|
|
4293
4529
|
result["build_response"] = prepared.get("build_response")
|
|
@@ -5426,7 +5662,8 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5426
5662
|
input_mint: str,
|
|
5427
5663
|
output_mint: str,
|
|
5428
5664
|
amount_ui: float,
|
|
5429
|
-
slippage_bps: int =
|
|
5665
|
+
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
5666
|
+
exclude_routers: list[str] | None = None,
|
|
5430
5667
|
) -> dict[str, Any]:
|
|
5431
5668
|
if self.network != "mainnet":
|
|
5432
5669
|
raise WalletBackendError("Provider-routed swaps are only enabled for Solana mainnet.")
|
|
@@ -5447,29 +5684,38 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5447
5684
|
raise WalletBackendError("amount is too small for the input token decimals.")
|
|
5448
5685
|
|
|
5449
5686
|
sender = await self.get_address()
|
|
5450
|
-
quote_source = "jupiter-
|
|
5687
|
+
quote_source = "jupiter-v2-order"
|
|
5451
5688
|
try:
|
|
5452
|
-
quote = await jupiter.
|
|
5689
|
+
quote = await jupiter.fetch_swap_v2_order(
|
|
5453
5690
|
input_mint=input_mint,
|
|
5454
5691
|
output_mint=output_mint,
|
|
5455
5692
|
amount_raw=raw_amount,
|
|
5456
5693
|
taker=sender,
|
|
5457
|
-
|
|
5694
|
+
exclude_routers=exclude_routers,
|
|
5458
5695
|
)
|
|
5459
5696
|
except ProviderError:
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5697
|
+
quote_source = "jupiter-ultra"
|
|
5698
|
+
try:
|
|
5699
|
+
quote = await jupiter.fetch_ultra_order(
|
|
5700
|
+
input_mint=input_mint,
|
|
5701
|
+
output_mint=output_mint,
|
|
5702
|
+
amount_raw=raw_amount,
|
|
5703
|
+
taker=sender,
|
|
5704
|
+
slippage_bps=slippage_bps,
|
|
5705
|
+
)
|
|
5706
|
+
except ProviderError:
|
|
5707
|
+
quote = await jupiter.fetch_quote(
|
|
5708
|
+
input_mint=input_mint,
|
|
5709
|
+
output_mint=output_mint,
|
|
5710
|
+
amount_raw=raw_amount,
|
|
5711
|
+
slippage_bps=slippage_bps,
|
|
5712
|
+
)
|
|
5713
|
+
quote_source = "jupiter-metis"
|
|
5467
5714
|
|
|
5468
5715
|
out_amount_raw = int(quote.get("outAmount") or 0)
|
|
5469
|
-
other_threshold_raw =
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
or 0
|
|
5716
|
+
other_threshold_raw = self._swap_minimum_output_floor(
|
|
5717
|
+
out_amount_raw=out_amount_raw,
|
|
5718
|
+
slippage_bps=slippage_bps,
|
|
5473
5719
|
)
|
|
5474
5720
|
fee_summary = self._build_swap_fee_summary(
|
|
5475
5721
|
swap_provider=quote_source,
|
|
@@ -5505,12 +5751,115 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5505
5751
|
"source": quote_source,
|
|
5506
5752
|
}
|
|
5507
5753
|
|
|
5754
|
+
async def preview_swap_intent(
|
|
5755
|
+
self,
|
|
5756
|
+
input_mint: str,
|
|
5757
|
+
output_mint: str,
|
|
5758
|
+
amount_ui: float,
|
|
5759
|
+
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
5760
|
+
minimum_output_amount_raw: int | None = None,
|
|
5761
|
+
max_fee_lamports: int | None = None,
|
|
5762
|
+
valid_for_seconds: int = 120,
|
|
5763
|
+
max_attempts: int = 3,
|
|
5764
|
+
) -> dict[str, Any]:
|
|
5765
|
+
if valid_for_seconds <= 0 or valid_for_seconds > 120:
|
|
5766
|
+
raise WalletBackendError("valid_for_seconds must be between 1 and 120.")
|
|
5767
|
+
if max_attempts <= 0 or max_attempts > 5:
|
|
5768
|
+
raise WalletBackendError("max_attempts must be between 1 and 5.")
|
|
5769
|
+
slippage_bps = max(int(slippage_bps), SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS)
|
|
5770
|
+
max_attempts = max(int(max_attempts), 3)
|
|
5771
|
+
|
|
5772
|
+
indicative = await self.preview_swap(
|
|
5773
|
+
input_mint=input_mint,
|
|
5774
|
+
output_mint=output_mint,
|
|
5775
|
+
amount_ui=amount_ui,
|
|
5776
|
+
slippage_bps=slippage_bps,
|
|
5777
|
+
)
|
|
5778
|
+
indicative_output_raw = int(indicative.get("estimated_output_amount_raw") or 0)
|
|
5779
|
+
slippage_floor_raw = self._swap_minimum_output_floor(
|
|
5780
|
+
out_amount_raw=indicative_output_raw,
|
|
5781
|
+
slippage_bps=slippage_bps,
|
|
5782
|
+
)
|
|
5783
|
+
requested_min_output_raw = (
|
|
5784
|
+
int(minimum_output_amount_raw)
|
|
5785
|
+
if minimum_output_amount_raw is not None
|
|
5786
|
+
else None
|
|
5787
|
+
)
|
|
5788
|
+
if requested_min_output_raw is not None:
|
|
5789
|
+
min_output_raw = min(requested_min_output_raw, slippage_floor_raw)
|
|
5790
|
+
minimum_output_policy = (
|
|
5791
|
+
"explicit_clamped_to_slippage_floor"
|
|
5792
|
+
if requested_min_output_raw > slippage_floor_raw
|
|
5793
|
+
else "explicit"
|
|
5794
|
+
)
|
|
5795
|
+
else:
|
|
5796
|
+
min_output_raw = slippage_floor_raw
|
|
5797
|
+
minimum_output_policy = "slippage_floor"
|
|
5798
|
+
if min_output_raw <= 0:
|
|
5799
|
+
raise WalletBackendError("minimum_output_amount_raw could not be derived from the indicative quote.")
|
|
5800
|
+
output_decimals = int(indicative.get("output_decimals") or 0)
|
|
5801
|
+
min_output_ui = min_output_raw / (10**output_decimals)
|
|
5802
|
+
|
|
5803
|
+
fee_summary = (
|
|
5804
|
+
indicative.get("fee_summary")
|
|
5805
|
+
if isinstance(indicative.get("fee_summary"), dict)
|
|
5806
|
+
else {}
|
|
5807
|
+
)
|
|
5808
|
+
fee_limit = (
|
|
5809
|
+
int(max_fee_lamports)
|
|
5810
|
+
if max_fee_lamports is not None
|
|
5811
|
+
else self._default_swap_intent_max_fee_lamports(fee_summary)
|
|
5812
|
+
)
|
|
5813
|
+
if fee_limit < 0:
|
|
5814
|
+
raise WalletBackendError("max_fee_lamports must be non-negative.")
|
|
5815
|
+
|
|
5816
|
+
return {
|
|
5817
|
+
"chain": "solana",
|
|
5818
|
+
"network": self.network,
|
|
5819
|
+
"mode": "intent_preview",
|
|
5820
|
+
"asset_type": "solana-swap-intent",
|
|
5821
|
+
"owner": indicative.get("owner"),
|
|
5822
|
+
"input_mint": indicative["input_mint"],
|
|
5823
|
+
"output_mint": indicative["output_mint"],
|
|
5824
|
+
"input_amount_ui": indicative["input_amount_ui"],
|
|
5825
|
+
"input_amount_raw": indicative["input_amount_raw"],
|
|
5826
|
+
"input_decimals": indicative.get("input_decimals"),
|
|
5827
|
+
"output_decimals": indicative.get("output_decimals"),
|
|
5828
|
+
"indicative_output_amount_ui": indicative.get("estimated_output_amount_ui"),
|
|
5829
|
+
"indicative_output_amount_raw": indicative.get("estimated_output_amount_raw"),
|
|
5830
|
+
"minimum_output_amount_ui": min_output_ui,
|
|
5831
|
+
"minimum_output_amount_raw": min_output_raw,
|
|
5832
|
+
"requested_minimum_output_amount_raw": requested_min_output_raw,
|
|
5833
|
+
"minimum_output_policy": minimum_output_policy,
|
|
5834
|
+
"max_slippage_bps": slippage_bps,
|
|
5835
|
+
"slippage_bps": slippage_bps,
|
|
5836
|
+
"max_fee_lamports": fee_limit,
|
|
5837
|
+
"max_fee_sol": fee_limit / solana_rpc.LAMPORTS_PER_SOL,
|
|
5838
|
+
"valid_for_seconds": valid_for_seconds,
|
|
5839
|
+
"valid_until_epoch_seconds": int(time.time()) + valid_for_seconds,
|
|
5840
|
+
"max_attempts": max_attempts,
|
|
5841
|
+
"allowed_providers": ["jupiter-v2-order", "jupiter-ultra", "jupiter-metis"],
|
|
5842
|
+
"recipient_policy": "owner-only",
|
|
5843
|
+
"spend_policy": "exact-input",
|
|
5844
|
+
"indicative_swap_provider": indicative.get("swap_provider"),
|
|
5845
|
+
"indicative_price_impact_pct": indicative.get("price_impact_pct"),
|
|
5846
|
+
"indicative_route_plan": indicative.get("route_plan", []),
|
|
5847
|
+
"indicative_fee_summary": fee_summary,
|
|
5848
|
+
"intent_note": (
|
|
5849
|
+
"This is an intent approval preview. Execute will fetch a fresh quote and "
|
|
5850
|
+
"only sign/send if it remains inside these approved limits."
|
|
5851
|
+
),
|
|
5852
|
+
"can_send": self.get_capabilities().can_send_transaction,
|
|
5853
|
+
"sign_only": self.sign_only,
|
|
5854
|
+
"source": "swap-intent",
|
|
5855
|
+
}
|
|
5856
|
+
|
|
5508
5857
|
async def execute_swap(
|
|
5509
5858
|
self,
|
|
5510
5859
|
input_mint: str,
|
|
5511
5860
|
output_mint: str,
|
|
5512
5861
|
amount_ui: float,
|
|
5513
|
-
slippage_bps: int =
|
|
5862
|
+
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
5514
5863
|
) -> dict[str, Any]:
|
|
5515
5864
|
preview = await self.preview_swap(
|
|
5516
5865
|
input_mint=input_mint,
|
|
@@ -5520,17 +5869,23 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5520
5869
|
)
|
|
5521
5870
|
return await self.execute_swap_from_preview(preview)
|
|
5522
5871
|
|
|
5523
|
-
async def
|
|
5872
|
+
async def _submit_prepared_swap(
|
|
5524
5873
|
self,
|
|
5525
|
-
|
|
5874
|
+
prepared: dict[str, Any],
|
|
5526
5875
|
) -> dict[str, Any]:
|
|
5527
|
-
prepared = await self.prepare_swap_from_preview(preview)
|
|
5528
5876
|
if self.sign_only:
|
|
5529
5877
|
raise WalletBackendError(
|
|
5530
5878
|
"This wallet backend is in sign-only mode. Disable sign_only to broadcast transactions."
|
|
5531
5879
|
)
|
|
5532
5880
|
|
|
5533
|
-
if prepared.get("swap_provider") == "jupiter-
|
|
5881
|
+
if prepared.get("swap_provider") == "jupiter-v2-order":
|
|
5882
|
+
submitted = await jupiter.execute_swap_v2_order(
|
|
5883
|
+
signed_transaction_base64=str(prepared["transaction_base64"]),
|
|
5884
|
+
request_id=str(prepared["request_id"]),
|
|
5885
|
+
last_valid_block_height=_coerce_int(prepared.get("last_valid_block_height")),
|
|
5886
|
+
)
|
|
5887
|
+
onchain_signature = submitted.get("signature") or submitted.get("txid")
|
|
5888
|
+
elif prepared.get("swap_provider") == "jupiter-ultra":
|
|
5534
5889
|
submitted = await jupiter.execute_ultra_order(
|
|
5535
5890
|
signed_transaction_base64=str(prepared["transaction_base64"]),
|
|
5536
5891
|
request_id=str(prepared["request_id"]),
|
|
@@ -5583,12 +5938,132 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5583
5938
|
"source": prepared.get("swap_provider") or "jupiter-metis",
|
|
5584
5939
|
}
|
|
5585
5940
|
|
|
5941
|
+
async def execute_swap_from_preview(
|
|
5942
|
+
self,
|
|
5943
|
+
preview: dict[str, Any],
|
|
5944
|
+
) -> dict[str, Any]:
|
|
5945
|
+
prepared = await self.prepare_swap_from_preview(preview)
|
|
5946
|
+
return await self._submit_prepared_swap(prepared)
|
|
5947
|
+
|
|
5948
|
+
async def execute_swap_intent(
|
|
5949
|
+
self,
|
|
5950
|
+
*,
|
|
5951
|
+
input_mint: str,
|
|
5952
|
+
output_mint: str,
|
|
5953
|
+
amount_ui: float,
|
|
5954
|
+
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
5955
|
+
minimum_output_amount_raw: int | None = None,
|
|
5956
|
+
max_fee_lamports: int | None = None,
|
|
5957
|
+
valid_until_epoch_seconds: int | None = None,
|
|
5958
|
+
max_attempts: int = 3,
|
|
5959
|
+
) -> dict[str, Any]:
|
|
5960
|
+
if valid_until_epoch_seconds is not None and int(time.time()) > int(valid_until_epoch_seconds):
|
|
5961
|
+
raise WalletBackendError("Approved swap intent has expired. Create a fresh intent preview.")
|
|
5962
|
+
if max_attempts <= 0 or max_attempts > 5:
|
|
5963
|
+
raise WalletBackendError("max_attempts must be between 1 and 5.")
|
|
5964
|
+
slippage_bps = max(int(slippage_bps), SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS)
|
|
5965
|
+
max_attempts = max(int(max_attempts), 3)
|
|
5966
|
+
|
|
5967
|
+
attempts: list[dict[str, Any]] = []
|
|
5968
|
+
last_error: str | None = None
|
|
5969
|
+
for attempt_index in range(max_attempts):
|
|
5970
|
+
if valid_until_epoch_seconds is not None and int(time.time()) > int(valid_until_epoch_seconds):
|
|
5971
|
+
break
|
|
5972
|
+
try:
|
|
5973
|
+
exclude_routers = ["jupiterz"] if attempt_index > 0 else None
|
|
5974
|
+
preview = await self.preview_swap(
|
|
5975
|
+
input_mint=input_mint,
|
|
5976
|
+
output_mint=output_mint,
|
|
5977
|
+
amount_ui=amount_ui,
|
|
5978
|
+
slippage_bps=slippage_bps,
|
|
5979
|
+
exclude_routers=exclude_routers,
|
|
5980
|
+
)
|
|
5981
|
+
estimated_output_raw = int(preview.get("estimated_output_amount_raw") or 0)
|
|
5982
|
+
if (
|
|
5983
|
+
minimum_output_amount_raw is not None
|
|
5984
|
+
and estimated_output_raw < int(minimum_output_amount_raw)
|
|
5985
|
+
):
|
|
5986
|
+
attempts.append(
|
|
5987
|
+
{
|
|
5988
|
+
"attempt": attempt_index + 1,
|
|
5989
|
+
"swap_provider": preview.get("swap_provider"),
|
|
5990
|
+
"rejected": "quote_below_minimum_output",
|
|
5991
|
+
"estimated_output_amount_raw": estimated_output_raw,
|
|
5992
|
+
"minimum_output_amount_raw": int(minimum_output_amount_raw),
|
|
5993
|
+
}
|
|
5994
|
+
)
|
|
5995
|
+
last_error = "Fresh swap quote is below the approved minimum output."
|
|
5996
|
+
continue
|
|
5997
|
+
|
|
5998
|
+
prepared = await self.prepare_swap_from_preview(preview)
|
|
5999
|
+
prepared_fee = self._swap_fee_lamports(prepared)
|
|
6000
|
+
if (
|
|
6001
|
+
max_fee_lamports is not None
|
|
6002
|
+
and prepared_fee is not None
|
|
6003
|
+
and prepared_fee > int(max_fee_lamports)
|
|
6004
|
+
):
|
|
6005
|
+
attempts.append(
|
|
6006
|
+
{
|
|
6007
|
+
"attempt": attempt_index + 1,
|
|
6008
|
+
"swap_provider": prepared.get("swap_provider"),
|
|
6009
|
+
"rejected": "fee_above_limit",
|
|
6010
|
+
"fee_lamports": prepared_fee,
|
|
6011
|
+
"max_fee_lamports": int(max_fee_lamports),
|
|
6012
|
+
}
|
|
6013
|
+
)
|
|
6014
|
+
last_error = "Fresh swap fee exceeds the approved fee limit."
|
|
6015
|
+
continue
|
|
6016
|
+
|
|
6017
|
+
result = await self._submit_prepared_swap(prepared)
|
|
6018
|
+
result["intent_execution"] = {
|
|
6019
|
+
"approved_minimum_output_amount_raw": minimum_output_amount_raw,
|
|
6020
|
+
"approved_max_fee_lamports": max_fee_lamports,
|
|
6021
|
+
"fresh_quote_used": True,
|
|
6022
|
+
"attempt_count": attempt_index + 1,
|
|
6023
|
+
"max_attempts": max_attempts,
|
|
6024
|
+
"attempts": attempts
|
|
6025
|
+
+ [
|
|
6026
|
+
{
|
|
6027
|
+
"attempt": attempt_index + 1,
|
|
6028
|
+
"swap_provider": prepared.get("swap_provider"),
|
|
6029
|
+
"status": "submitted",
|
|
6030
|
+
}
|
|
6031
|
+
],
|
|
6032
|
+
}
|
|
6033
|
+
return result
|
|
6034
|
+
except (WalletBackendError, ProviderError) as exc:
|
|
6035
|
+
last_error = str(exc)
|
|
6036
|
+
attempts.append(
|
|
6037
|
+
{
|
|
6038
|
+
"attempt": attempt_index + 1,
|
|
6039
|
+
"rejected": "execution_error",
|
|
6040
|
+
"error": str(exc),
|
|
6041
|
+
}
|
|
6042
|
+
)
|
|
6043
|
+
if "sign-only mode" in str(exc).lower():
|
|
6044
|
+
break
|
|
6045
|
+
if attempt_index + 1 < max_attempts:
|
|
6046
|
+
await asyncio.sleep(min(0.5 * (attempt_index + 1), 1.5))
|
|
6047
|
+
|
|
6048
|
+
reason_suffix = f" Last reason: {last_error}" if last_error else ""
|
|
6049
|
+
raise WalletBackendError(
|
|
6050
|
+
"Solana swap intent execution failed within the approved limits. Funds were not moved."
|
|
6051
|
+
+ reason_suffix,
|
|
6052
|
+
details={
|
|
6053
|
+
"reason": last_error,
|
|
6054
|
+
"attempts": attempts,
|
|
6055
|
+
"minimum_output_amount_raw": minimum_output_amount_raw,
|
|
6056
|
+
"max_fee_lamports": max_fee_lamports,
|
|
6057
|
+
"max_attempts": max_attempts,
|
|
6058
|
+
},
|
|
6059
|
+
)
|
|
6060
|
+
|
|
5586
6061
|
async def prepare_swap(
|
|
5587
6062
|
self,
|
|
5588
6063
|
input_mint: str,
|
|
5589
6064
|
output_mint: str,
|
|
5590
6065
|
amount_ui: float,
|
|
5591
|
-
slippage_bps: int =
|
|
6066
|
+
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
5592
6067
|
) -> dict[str, Any]:
|
|
5593
6068
|
preview = await self.preview_swap(
|
|
5594
6069
|
input_mint=input_mint,
|
|
@@ -5629,13 +6104,23 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5629
6104
|
|
|
5630
6105
|
swap_provider = str(preview.get("swap_provider") or "jupiter-metis")
|
|
5631
6106
|
request_id = None
|
|
5632
|
-
if swap_provider
|
|
6107
|
+
if swap_provider in {"jupiter-v2-order", "jupiter-ultra"}:
|
|
5633
6108
|
swap_build = preview["quote_response"]
|
|
5634
6109
|
unsigned_transaction = VersionedTransaction.from_bytes(
|
|
5635
6110
|
base64.b64decode(str(swap_build["transaction"]))
|
|
5636
6111
|
)
|
|
5637
6112
|
request_id = swap_build.get("requestId")
|
|
5638
|
-
|
|
6113
|
+
blockhash_metadata = swap_build.get("blockhashWithMetadata")
|
|
6114
|
+
last_valid_block_height = (
|
|
6115
|
+
blockhash_metadata.get("lastValidBlockHeight")
|
|
6116
|
+
if isinstance(blockhash_metadata, dict)
|
|
6117
|
+
else None
|
|
6118
|
+
)
|
|
6119
|
+
if last_valid_block_height is None:
|
|
6120
|
+
last_valid_block_height = (
|
|
6121
|
+
swap_build.get("lastValidBlockHeight")
|
|
6122
|
+
or swap_build.get("expireAt")
|
|
6123
|
+
)
|
|
5639
6124
|
prioritization_fee_lamports = swap_build.get("prioritizationFeeLamports")
|
|
5640
6125
|
compute_unit_limit = swap_build.get("computeUnitLimit")
|
|
5641
6126
|
else:
|