@agentlayer.tech/wallet 0.1.26 → 0.1.28
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/dist/index.js +5 -11
- package/.openclaw/extensions/agent-wallet/index.ts +5 -11
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +1 -0
- package/CHANGELOG.md +26 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +99 -84
- package/agent-wallet/agent_wallet/providers/jupiter.py +38 -14
- package/agent-wallet/agent_wallet/providers/kamino.py +6 -0
- package/agent-wallet/agent_wallet/providers/x402.py +194 -9
- package/agent-wallet/agent_wallet/transaction_policy.py +6 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +20 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +230 -20
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +11 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +1 -0
- package/package.json +1 -1
|
@@ -3765,19 +3765,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3765
3765
|
keypair = Keypair.from_bytes(self.signer.export_keypair_bytes())
|
|
3766
3766
|
try:
|
|
3767
3767
|
unsigned_transaction = VersionedTransaction.from_bytes(raw_transaction)
|
|
3768
|
-
|
|
3769
|
-
signatures = list(unsigned_transaction.signatures)
|
|
3770
|
-
if wallet_signer_index >= len(signatures):
|
|
3771
|
-
raise WalletBackendError(
|
|
3772
|
-
"Provider transaction signer layout is incompatible with local signing."
|
|
3773
|
-
)
|
|
3774
|
-
signatures[wallet_signer_index] = signature
|
|
3775
|
-
signed_transaction = VersionedTransaction.populate(
|
|
3776
|
-
unsigned_transaction.message,
|
|
3777
|
-
signatures,
|
|
3778
|
-
)
|
|
3779
|
-
return encode_transaction_base64(bytes(signed_transaction))
|
|
3780
|
-
except Exception:
|
|
3768
|
+
except (TypeError, ValueError):
|
|
3781
3769
|
unsigned_transaction = Transaction.from_bytes(raw_transaction)
|
|
3782
3770
|
signatures = list(unsigned_transaction.signatures)
|
|
3783
3771
|
if wallet_signer_index >= len(signatures):
|
|
@@ -3789,6 +3777,18 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3789
3777
|
unsigned_transaction.message.recent_blockhash,
|
|
3790
3778
|
)
|
|
3791
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))
|
|
3792
3792
|
|
|
3793
3793
|
async def _prepare_jupiter_lend_transaction(
|
|
3794
3794
|
self,
|
|
@@ -3847,9 +3847,11 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3847
3847
|
raise WalletBackendError(
|
|
3848
3848
|
"This wallet backend is in sign-only mode. Disable sign_only to broadcast transactions."
|
|
3849
3849
|
)
|
|
3850
|
+
kamino_verified = bool((prepared.get("kamino_safety") or {}).get("verified"))
|
|
3850
3851
|
submitted = await solana_rpc.send_transaction(
|
|
3851
3852
|
transaction_base64=str(prepared["transaction_base64"]),
|
|
3852
3853
|
rpc_url=self.rpc_urls,
|
|
3854
|
+
skip_preflight=source == "kamino" and kamino_verified,
|
|
3853
3855
|
)
|
|
3854
3856
|
signature = submitted.get("signature")
|
|
3855
3857
|
status = None
|
|
@@ -3858,6 +3860,8 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3858
3860
|
status = await solana_rpc.wait_for_confirmation(
|
|
3859
3861
|
signature=signature,
|
|
3860
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,
|
|
3861
3865
|
)
|
|
3862
3866
|
confirmed = status is not None
|
|
3863
3867
|
return {
|
|
@@ -3875,6 +3879,8 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3875
3879
|
"slot": status.get("slot") if status else None,
|
|
3876
3880
|
"sign_only": self.sign_only,
|
|
3877
3881
|
"source": source,
|
|
3882
|
+
"simulation": prepared.get("simulation"),
|
|
3883
|
+
"kamino_safety": prepared.get("kamino_safety"),
|
|
3878
3884
|
}
|
|
3879
3885
|
|
|
3880
3886
|
async def _execute_prepared_jupiter_lend_transaction(self, prepared: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -3925,6 +3931,37 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3925
3931
|
matches.append(item)
|
|
3926
3932
|
return matches
|
|
3927
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
|
+
|
|
3928
3965
|
async def _prepare_kamino_lend_transaction(
|
|
3929
3966
|
self,
|
|
3930
3967
|
*,
|
|
@@ -3933,6 +3970,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3933
3970
|
market: str,
|
|
3934
3971
|
reserve: str,
|
|
3935
3972
|
amount_ui: str,
|
|
3973
|
+
obligation_address: str | None = None,
|
|
3936
3974
|
) -> dict[str, Any]:
|
|
3937
3975
|
if not self.signer:
|
|
3938
3976
|
raise WalletBackendError("Solana signer is not configured.")
|
|
@@ -3953,12 +3991,49 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3953
3991
|
market_address=market,
|
|
3954
3992
|
reserve_address=reserve,
|
|
3955
3993
|
action=f"Kamino {action}",
|
|
3994
|
+
obligation_address=obligation_address,
|
|
3956
3995
|
loaded_addresses=loaded_addresses,
|
|
3957
3996
|
)
|
|
3958
3997
|
signed_transaction_base64 = await self._sign_versioned_provider_transaction(
|
|
3959
3998
|
transaction_base64=transaction_base64,
|
|
3960
3999
|
wallet_signer_index=int(verification.get("wallet_signer_index") or 0),
|
|
3961
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
|
+
}
|
|
3962
4037
|
return {
|
|
3963
4038
|
"chain": "solana",
|
|
3964
4039
|
"network": self.network,
|
|
@@ -3967,6 +4042,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3967
4042
|
"owner": owner,
|
|
3968
4043
|
"market": market,
|
|
3969
4044
|
"reserve": reserve,
|
|
4045
|
+
"obligation_address": obligation_address,
|
|
3970
4046
|
"amount_ui": amount_ui,
|
|
3971
4047
|
"transaction_base64": signed_transaction_base64,
|
|
3972
4048
|
"transaction_encoding": "base64",
|
|
@@ -3975,15 +4051,30 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3975
4051
|
"broadcasted": False,
|
|
3976
4052
|
"confirmed": False,
|
|
3977
4053
|
"verification": verification,
|
|
4054
|
+
"simulation": simulation_value,
|
|
4055
|
+
"kamino_safety": kamino_safety,
|
|
3978
4056
|
"sign_only": self.sign_only,
|
|
3979
4057
|
"source": "kamino",
|
|
3980
4058
|
}
|
|
3981
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
|
+
|
|
3982
4072
|
async def preview_kamino_lend_deposit(
|
|
3983
4073
|
self,
|
|
3984
4074
|
market: str,
|
|
3985
4075
|
reserve: str,
|
|
3986
4076
|
amount_ui: str,
|
|
4077
|
+
obligation_address: str | None = None,
|
|
3987
4078
|
) -> dict[str, Any]:
|
|
3988
4079
|
self._require_mainnet_kamino("Kamino lending")
|
|
3989
4080
|
owner = await self.get_address()
|
|
@@ -4021,8 +4112,13 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4021
4112
|
market: str,
|
|
4022
4113
|
reserve: str,
|
|
4023
4114
|
amount_ui: str,
|
|
4115
|
+
obligation_address: str | None = None,
|
|
4116
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4024
4117
|
) -> dict[str, Any]:
|
|
4025
|
-
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(
|
|
4026
4122
|
market=market,
|
|
4027
4123
|
reserve=reserve,
|
|
4028
4124
|
amount_ui=amount_ui,
|
|
@@ -4049,11 +4145,14 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4049
4145
|
market: str,
|
|
4050
4146
|
reserve: str,
|
|
4051
4147
|
amount_ui: str,
|
|
4148
|
+
obligation_address: str | None = None,
|
|
4149
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4052
4150
|
) -> dict[str, Any]:
|
|
4053
4151
|
prepared = await self.prepare_kamino_lend_deposit(
|
|
4054
4152
|
market=market,
|
|
4055
4153
|
reserve=reserve,
|
|
4056
4154
|
amount_ui=amount_ui,
|
|
4155
|
+
approved_preview=approved_preview,
|
|
4057
4156
|
)
|
|
4058
4157
|
result = await self._execute_prepared_provider_transaction(prepared, source="kamino")
|
|
4059
4158
|
result["build_response"] = prepared.get("build_response")
|
|
@@ -4064,6 +4163,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4064
4163
|
market: str,
|
|
4065
4164
|
reserve: str,
|
|
4066
4165
|
amount_ui: str,
|
|
4166
|
+
obligation_address: str | None = None,
|
|
4067
4167
|
) -> dict[str, Any]:
|
|
4068
4168
|
self._require_mainnet_kamino("Kamino lending")
|
|
4069
4169
|
owner = await self.get_address()
|
|
@@ -4088,6 +4188,19 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4088
4188
|
)
|
|
4089
4189
|
if not obligation_matches:
|
|
4090
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
|
+
)
|
|
4091
4204
|
return {
|
|
4092
4205
|
"chain": "solana",
|
|
4093
4206
|
"network": self.network,
|
|
@@ -4098,7 +4211,14 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4098
4211
|
"reserve": reserve,
|
|
4099
4212
|
"amount_ui": amount_ui,
|
|
4100
4213
|
"reserve_info": reserve_entry,
|
|
4101
|
-
"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,
|
|
4102
4222
|
"sign_only": self.sign_only,
|
|
4103
4223
|
"can_send": self.get_capabilities().can_send_transaction,
|
|
4104
4224
|
"source": "kamino",
|
|
@@ -4109,12 +4229,23 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4109
4229
|
market: str,
|
|
4110
4230
|
reserve: str,
|
|
4111
4231
|
amount_ui: str,
|
|
4232
|
+
obligation_address: str | None = None,
|
|
4233
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4112
4234
|
) -> dict[str, Any]:
|
|
4113
|
-
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(
|
|
4114
4239
|
market=market,
|
|
4115
4240
|
reserve=reserve,
|
|
4116
4241
|
amount_ui=amount_ui,
|
|
4242
|
+
obligation_address=obligation_address,
|
|
4117
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
|
+
)
|
|
4118
4249
|
owner = str(preview["owner"])
|
|
4119
4250
|
build = await kamino.build_lend_withdraw_transaction(
|
|
4120
4251
|
wallet=owner,
|
|
@@ -4128,6 +4259,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4128
4259
|
market=str(preview["market"]),
|
|
4129
4260
|
reserve=str(preview["reserve"]),
|
|
4130
4261
|
amount_ui=str(preview["amount_ui"]),
|
|
4262
|
+
obligation_address=selected_obligation_address or None,
|
|
4131
4263
|
)
|
|
4132
4264
|
prepared["build_response"] = build
|
|
4133
4265
|
return prepared
|
|
@@ -4137,11 +4269,15 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4137
4269
|
market: str,
|
|
4138
4270
|
reserve: str,
|
|
4139
4271
|
amount_ui: str,
|
|
4272
|
+
obligation_address: str | None = None,
|
|
4273
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4140
4274
|
) -> dict[str, Any]:
|
|
4141
4275
|
prepared = await self.prepare_kamino_lend_withdraw(
|
|
4142
4276
|
market=market,
|
|
4143
4277
|
reserve=reserve,
|
|
4144
4278
|
amount_ui=amount_ui,
|
|
4279
|
+
obligation_address=obligation_address,
|
|
4280
|
+
approved_preview=approved_preview,
|
|
4145
4281
|
)
|
|
4146
4282
|
result = await self._execute_prepared_provider_transaction(prepared, source="kamino")
|
|
4147
4283
|
result["build_response"] = prepared.get("build_response")
|
|
@@ -4152,6 +4288,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4152
4288
|
market: str,
|
|
4153
4289
|
reserve: str,
|
|
4154
4290
|
amount_ui: str,
|
|
4291
|
+
obligation_address: str | None = None,
|
|
4155
4292
|
) -> dict[str, Any]:
|
|
4156
4293
|
self._require_mainnet_kamino("Kamino lending")
|
|
4157
4294
|
owner = await self.get_address()
|
|
@@ -4172,6 +4309,19 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4172
4309
|
obligations = await self.get_kamino_lend_user_obligations(market=market, user=owner)
|
|
4173
4310
|
if int(obligations["obligation_count"]) <= 0:
|
|
4174
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
|
+
)
|
|
4175
4325
|
return {
|
|
4176
4326
|
"chain": "solana",
|
|
4177
4327
|
"network": self.network,
|
|
@@ -4182,7 +4332,14 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4182
4332
|
"reserve": reserve,
|
|
4183
4333
|
"amount_ui": amount_ui,
|
|
4184
4334
|
"reserve_info": reserve_entry,
|
|
4185
|
-
"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,
|
|
4186
4343
|
"sign_only": self.sign_only,
|
|
4187
4344
|
"can_send": self.get_capabilities().can_send_transaction,
|
|
4188
4345
|
"source": "kamino",
|
|
@@ -4193,12 +4350,23 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4193
4350
|
market: str,
|
|
4194
4351
|
reserve: str,
|
|
4195
4352
|
amount_ui: str,
|
|
4353
|
+
obligation_address: str | None = None,
|
|
4354
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4196
4355
|
) -> dict[str, Any]:
|
|
4197
|
-
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(
|
|
4198
4360
|
market=market,
|
|
4199
4361
|
reserve=reserve,
|
|
4200
4362
|
amount_ui=amount_ui,
|
|
4363
|
+
obligation_address=obligation_address,
|
|
4201
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
|
+
)
|
|
4202
4370
|
owner = str(preview["owner"])
|
|
4203
4371
|
build = await kamino.build_lend_borrow_transaction(
|
|
4204
4372
|
wallet=owner,
|
|
@@ -4212,6 +4380,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4212
4380
|
market=str(preview["market"]),
|
|
4213
4381
|
reserve=str(preview["reserve"]),
|
|
4214
4382
|
amount_ui=str(preview["amount_ui"]),
|
|
4383
|
+
obligation_address=selected_obligation_address or None,
|
|
4215
4384
|
)
|
|
4216
4385
|
prepared["build_response"] = build
|
|
4217
4386
|
return prepared
|
|
@@ -4221,11 +4390,15 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4221
4390
|
market: str,
|
|
4222
4391
|
reserve: str,
|
|
4223
4392
|
amount_ui: str,
|
|
4393
|
+
obligation_address: str | None = None,
|
|
4394
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4224
4395
|
) -> dict[str, Any]:
|
|
4225
4396
|
prepared = await self.prepare_kamino_lend_borrow(
|
|
4226
4397
|
market=market,
|
|
4227
4398
|
reserve=reserve,
|
|
4228
4399
|
amount_ui=amount_ui,
|
|
4400
|
+
obligation_address=obligation_address,
|
|
4401
|
+
approved_preview=approved_preview,
|
|
4229
4402
|
)
|
|
4230
4403
|
result = await self._execute_prepared_provider_transaction(prepared, source="kamino")
|
|
4231
4404
|
result["build_response"] = prepared.get("build_response")
|
|
@@ -4236,6 +4409,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4236
4409
|
market: str,
|
|
4237
4410
|
reserve: str,
|
|
4238
4411
|
amount_ui: str,
|
|
4412
|
+
obligation_address: str | None = None,
|
|
4239
4413
|
) -> dict[str, Any]:
|
|
4240
4414
|
self._require_mainnet_kamino("Kamino lending")
|
|
4241
4415
|
owner = await self.get_address()
|
|
@@ -4260,6 +4434,19 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4260
4434
|
)
|
|
4261
4435
|
if not obligation_matches:
|
|
4262
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
|
+
)
|
|
4263
4450
|
return {
|
|
4264
4451
|
"chain": "solana",
|
|
4265
4452
|
"network": self.network,
|
|
@@ -4270,7 +4457,14 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4270
4457
|
"reserve": reserve,
|
|
4271
4458
|
"amount_ui": amount_ui,
|
|
4272
4459
|
"reserve_info": reserve_entry,
|
|
4273
|
-
"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,
|
|
4274
4468
|
"sign_only": self.sign_only,
|
|
4275
4469
|
"can_send": self.get_capabilities().can_send_transaction,
|
|
4276
4470
|
"source": "kamino",
|
|
@@ -4281,12 +4475,23 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4281
4475
|
market: str,
|
|
4282
4476
|
reserve: str,
|
|
4283
4477
|
amount_ui: str,
|
|
4478
|
+
obligation_address: str | None = None,
|
|
4479
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4284
4480
|
) -> dict[str, Any]:
|
|
4285
|
-
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(
|
|
4286
4485
|
market=market,
|
|
4287
4486
|
reserve=reserve,
|
|
4288
4487
|
amount_ui=amount_ui,
|
|
4488
|
+
obligation_address=obligation_address,
|
|
4289
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
|
+
)
|
|
4290
4495
|
owner = str(preview["owner"])
|
|
4291
4496
|
build = await kamino.build_lend_repay_transaction(
|
|
4292
4497
|
wallet=owner,
|
|
@@ -4300,6 +4505,7 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4300
4505
|
market=str(preview["market"]),
|
|
4301
4506
|
reserve=str(preview["reserve"]),
|
|
4302
4507
|
amount_ui=str(preview["amount_ui"]),
|
|
4508
|
+
obligation_address=selected_obligation_address or None,
|
|
4303
4509
|
)
|
|
4304
4510
|
prepared["build_response"] = build
|
|
4305
4511
|
return prepared
|
|
@@ -4309,11 +4515,15 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
4309
4515
|
market: str,
|
|
4310
4516
|
reserve: str,
|
|
4311
4517
|
amount_ui: str,
|
|
4518
|
+
obligation_address: str | None = None,
|
|
4519
|
+
approved_preview: dict[str, Any] | None = None,
|
|
4312
4520
|
) -> dict[str, Any]:
|
|
4313
4521
|
prepared = await self.prepare_kamino_lend_repay(
|
|
4314
4522
|
market=market,
|
|
4315
4523
|
reserve=reserve,
|
|
4316
4524
|
amount_ui=amount_ui,
|
|
4525
|
+
obligation_address=obligation_address,
|
|
4526
|
+
approved_preview=approved_preview,
|
|
4317
4527
|
)
|
|
4318
4528
|
result = await self._execute_prepared_provider_transaction(prepared, source="kamino")
|
|
4319
4529
|
result["build_response"] = prepared.get("build_response")
|
|
@@ -33,6 +33,16 @@ def _lifi_chain_id_for_evm_network(network: str) -> str:
|
|
|
33
33
|
return "1"
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _normalize_x402_typed_data_value(value: Any) -> Any:
|
|
37
|
+
if isinstance(value, (bytes, bytearray)):
|
|
38
|
+
return "0x" + bytes(value).hex()
|
|
39
|
+
if isinstance(value, list):
|
|
40
|
+
return [_normalize_x402_typed_data_value(item) for item in value]
|
|
41
|
+
if isinstance(value, dict):
|
|
42
|
+
return {str(key): _normalize_x402_typed_data_value(item) for key, item in value.items()}
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
36
46
|
def _extract_fee_wei(payload: dict[str, Any]) -> str | None:
|
|
37
47
|
for key in ("fee", "maxFee", "totalFee", "gasCost", "cost"):
|
|
38
48
|
value = payload.get(key)
|
|
@@ -457,7 +467,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
457
467
|
"domain": domain,
|
|
458
468
|
"types": types,
|
|
459
469
|
"primaryType": primary_type,
|
|
460
|
-
"message": message,
|
|
470
|
+
"message": _normalize_x402_typed_data_value(message),
|
|
461
471
|
},
|
|
462
472
|
)
|
|
463
473
|
signature = str(data.get("signature") or "").strip()
|
|
@@ -80,6 +80,7 @@ Use this skill before calling OpenClaw wallet tools. It is the routing guide for
|
|
|
80
80
|
- Prefer `mode=intent_preview`, show the intent limits to the user, then after chat confirmation call `mode=intent_execute` with the same semantic params. This confirms risk limits, not a stale quote fingerprint.
|
|
81
81
|
- Default Solana swap slippage is 300 bps (3%). The backend computes the approved minimum output from the indicative output and slippage, not from a strict RFQ threshold.
|
|
82
82
|
- The primary execution path uses Jupiter Swap API V2 `/order` + `/execute`; if a JupiterZ/RFQ route fails, the backend retries with a non-JupiterZ route when possible.
|
|
83
|
+
- Metis `/swap` fallback builds use Jupiter dynamic slippage and a bounded `veryHigh` priority fee instead of the old `"auto"` priority mode.
|
|
83
84
|
- Do not use legacy `execute` for Solana Jupiter swaps in OpenClaw; exact quote-bound approval is too fragile for active markets.
|
|
84
85
|
- Use for SOL<->SPL or SPL<->SPL on Solana. Do not use LI.FI for Solana-only swaps.
|
|
85
86
|
- EVM same-chain Velora swap: `swap_evm_tokens`
|