@agentlayer.tech/wallet 0.1.26 → 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/dist/index.js +3 -0
- package/.openclaw/extensions/agent-wallet/index.ts +3 -0
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +1 -0
- package/CHANGELOG.md +12 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +55 -8
- 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/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/pyproject.toml +1 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +1 -0
- package/package.json +1 -1
|
@@ -1535,6 +1535,7 @@ const solanaToolDefinitions = [
|
|
|
1535
1535
|
market: { type: "string" },
|
|
1536
1536
|
reserve: { type: "string" },
|
|
1537
1537
|
amount_ui: { type: "string" },
|
|
1538
|
+
obligation_address: { type: "string" },
|
|
1538
1539
|
mode: { type: "string", enum: ["preview", "prepare", "execute"] },
|
|
1539
1540
|
purpose: { type: "string" },
|
|
1540
1541
|
user_intent: { type: "boolean" },
|
|
@@ -1553,6 +1554,7 @@ const solanaToolDefinitions = [
|
|
|
1553
1554
|
market: { type: "string" },
|
|
1554
1555
|
reserve: { type: "string" },
|
|
1555
1556
|
amount_ui: { type: "string" },
|
|
1557
|
+
obligation_address: { type: "string" },
|
|
1556
1558
|
mode: { type: "string", enum: ["preview", "prepare", "execute"] },
|
|
1557
1559
|
purpose: { type: "string" },
|
|
1558
1560
|
user_intent: { type: "boolean" },
|
|
@@ -1571,6 +1573,7 @@ const solanaToolDefinitions = [
|
|
|
1571
1573
|
market: { type: "string" },
|
|
1572
1574
|
reserve: { type: "string" },
|
|
1573
1575
|
amount_ui: { type: "string" },
|
|
1576
|
+
obligation_address: { type: "string" },
|
|
1574
1577
|
mode: { type: "string", enum: ["preview", "prepare", "execute"] },
|
|
1575
1578
|
purpose: { type: "string" },
|
|
1576
1579
|
user_intent: { type: "boolean" },
|
|
@@ -1535,6 +1535,7 @@ const solanaToolDefinitions = [
|
|
|
1535
1535
|
market: { type: "string" },
|
|
1536
1536
|
reserve: { type: "string" },
|
|
1537
1537
|
amount_ui: { type: "string" },
|
|
1538
|
+
obligation_address: { type: "string" },
|
|
1538
1539
|
mode: { type: "string", enum: ["preview", "prepare", "execute"] },
|
|
1539
1540
|
purpose: { type: "string" },
|
|
1540
1541
|
user_intent: { type: "boolean" },
|
|
@@ -1553,6 +1554,7 @@ const solanaToolDefinitions = [
|
|
|
1553
1554
|
market: { type: "string" },
|
|
1554
1555
|
reserve: { type: "string" },
|
|
1555
1556
|
amount_ui: { type: "string" },
|
|
1557
|
+
obligation_address: { type: "string" },
|
|
1556
1558
|
mode: { type: "string", enum: ["preview", "prepare", "execute"] },
|
|
1557
1559
|
purpose: { type: "string" },
|
|
1558
1560
|
user_intent: { type: "boolean" },
|
|
@@ -1571,6 +1573,7 @@ const solanaToolDefinitions = [
|
|
|
1571
1573
|
market: { type: "string" },
|
|
1572
1574
|
reserve: { type: "string" },
|
|
1573
1575
|
amount_ui: { type: "string" },
|
|
1576
|
+
obligation_address: { type: "string" },
|
|
1574
1577
|
mode: { type: "string", enum: ["preview", "prepare", "execute"] },
|
|
1575
1578
|
purpose: { type: "string" },
|
|
1576
1579
|
user_intent: { type: "boolean" },
|
|
@@ -13,6 +13,7 @@ Safety rules:
|
|
|
13
13
|
- For transfers, native staking, swaps, Aave writes, Jupiter Earn writes, and Kamino writes, use `preview` before `prepare` or `execute`.
|
|
14
14
|
- For Solana Jupiter swaps through `swap_solana_tokens`, prefer `intent_preview` then `intent_execute` after explicit chat confirmation. The user confirms risk limits; the backend refreshes the quote and only executes inside those limits.
|
|
15
15
|
- Solana swap intent defaults to at least 300 bps (3%) slippage, 120 seconds validity, and 3 fresh execution attempts. The backend computes the approved minimum output from the indicative output and slippage, clamps hand-tightened minimums to that floor, then executes through Jupiter Swap API V2 `/order` + `/execute` when available.
|
|
16
|
+
- Metis `/swap` fallback builds use Jupiter dynamic slippage and a bounded `veryHigh` priority fee instead of the old `"auto"` priority mode.
|
|
16
17
|
- Do not use legacy `execute` for Solana Jupiter swaps in OpenClaw. Exact quote-bound approval is too fragile for active markets and will be rejected by the bridge.
|
|
17
18
|
- For `swap_solana_privately`, use `preview` and then `execute` after explicit user approval. Do not use `prepare` for this tool.
|
|
18
19
|
- Use `prepare` only when the user clearly intends to produce an execution plan.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.1.27 - 2026-05-27
|
|
6
|
+
|
|
7
|
+
- Improved Solana swap fallback landing by enabling Jupiter dynamic slippage
|
|
8
|
+
and bounded `veryHigh` priority fees on the Metis `/swap` fallback path.
|
|
9
|
+
- Hardened Kamino transaction execution with local simulation before send,
|
|
10
|
+
Kamino-specific build timeouts, and longer confirmation polling on mainnet.
|
|
11
|
+
- Reused approved Kamino preview payloads during execute so OpenClaw no longer
|
|
12
|
+
needs to rebuild the same write path just to satisfy approval binding.
|
|
13
|
+
- Added Kamino obligation pinning for `withdraw`, `borrow`, and `repay`, so
|
|
14
|
+
preview can require an explicit `obligation_address` and execute verifies the
|
|
15
|
+
built transaction references the selected obligation before signing.
|
|
16
|
+
|
|
5
17
|
## v0.1.26 - 2026-05-26
|
|
6
18
|
|
|
7
19
|
- Reworked Solana Jupiter swaps to prefer intent approvals, so OpenClaw confirms
|
|
@@ -760,6 +760,7 @@ class OpenClawWalletAdapter:
|
|
|
760
760
|
"owner",
|
|
761
761
|
"authority",
|
|
762
762
|
"address",
|
|
763
|
+
"obligation_address",
|
|
763
764
|
"market",
|
|
764
765
|
"reserve",
|
|
765
766
|
"amount_native",
|
|
@@ -2969,6 +2970,10 @@ class OpenClawWalletAdapter:
|
|
|
2969
2970
|
"type": "string",
|
|
2970
2971
|
"description": "Decimal token amount to withdraw, as a string.",
|
|
2971
2972
|
},
|
|
2973
|
+
"obligation_address": {
|
|
2974
|
+
"type": "string",
|
|
2975
|
+
"description": "Optional Kamino obligation address. Required when preview shows multiple matching obligations.",
|
|
2976
|
+
},
|
|
2972
2977
|
"mode": {
|
|
2973
2978
|
"type": "string",
|
|
2974
2979
|
"enum": ["preview", "prepare", "execute"],
|
|
@@ -3003,6 +3008,10 @@ class OpenClawWalletAdapter:
|
|
|
3003
3008
|
"type": "string",
|
|
3004
3009
|
"description": "Decimal token amount to borrow, as a string.",
|
|
3005
3010
|
},
|
|
3011
|
+
"obligation_address": {
|
|
3012
|
+
"type": "string",
|
|
3013
|
+
"description": "Optional Kamino obligation address. Required when preview shows multiple obligations in the selected market.",
|
|
3014
|
+
},
|
|
3006
3015
|
"mode": {
|
|
3007
3016
|
"type": "string",
|
|
3008
3017
|
"enum": ["preview", "prepare", "execute"],
|
|
@@ -3037,6 +3046,10 @@ class OpenClawWalletAdapter:
|
|
|
3037
3046
|
"type": "string",
|
|
3038
3047
|
"description": "Decimal token amount to repay, as a string.",
|
|
3039
3048
|
},
|
|
3049
|
+
"obligation_address": {
|
|
3050
|
+
"type": "string",
|
|
3051
|
+
"description": "Optional Kamino obligation address. Required when preview shows multiple matching debt obligations.",
|
|
3052
|
+
},
|
|
3040
3053
|
"mode": {
|
|
3041
3054
|
"type": "string",
|
|
3042
3055
|
"enum": ["preview", "prepare", "execute"],
|
|
@@ -5003,6 +5016,7 @@ class OpenClawWalletAdapter:
|
|
|
5003
5016
|
market = args.get("market")
|
|
5004
5017
|
reserve = args.get("reserve")
|
|
5005
5018
|
amount_ui = args.get("amount_ui")
|
|
5019
|
+
obligation_address = args.get("obligation_address")
|
|
5006
5020
|
mode = args.get("mode")
|
|
5007
5021
|
purpose = args.get("purpose")
|
|
5008
5022
|
user_intent = args.get("user_intent", False)
|
|
@@ -5014,6 +5028,8 @@ class OpenClawWalletAdapter:
|
|
|
5014
5028
|
raise WalletBackendError("reserve is required.")
|
|
5015
5029
|
if not isinstance(amount_ui, str) or not amount_ui.strip():
|
|
5016
5030
|
raise WalletBackendError("amount_ui is required.")
|
|
5031
|
+
if obligation_address is not None and not isinstance(obligation_address, str):
|
|
5032
|
+
raise WalletBackendError("obligation_address must be a string when provided.")
|
|
5017
5033
|
if mode not in {"preview", "prepare", "execute"}:
|
|
5018
5034
|
raise WalletBackendError("mode must be 'preview', 'prepare' or 'execute'.")
|
|
5019
5035
|
if not isinstance(purpose, str) or not purpose.strip():
|
|
@@ -5046,6 +5062,7 @@ class OpenClawWalletAdapter:
|
|
|
5046
5062
|
market=market.strip(),
|
|
5047
5063
|
reserve=reserve.strip(),
|
|
5048
5064
|
amount_ui=amount_ui.strip(),
|
|
5065
|
+
obligation_address=obligation_address.strip() if isinstance(obligation_address, str) and obligation_address.strip() else None,
|
|
5049
5066
|
)
|
|
5050
5067
|
return AgentToolResult(
|
|
5051
5068
|
tool=tool_name,
|
|
@@ -5063,7 +5080,12 @@ class OpenClawWalletAdapter:
|
|
|
5063
5080
|
market=market.strip(),
|
|
5064
5081
|
reserve=reserve.strip(),
|
|
5065
5082
|
amount_ui=amount_ui.strip(),
|
|
5083
|
+
obligation_address=obligation_address.strip() if isinstance(obligation_address, str) and obligation_address.strip() else None,
|
|
5066
5084
|
)
|
|
5085
|
+
if bool(preview.get("requires_obligation_address")):
|
|
5086
|
+
raise WalletBackendError(
|
|
5087
|
+
f"{action_label} requires obligation_address when multiple Kamino obligations match the selected position."
|
|
5088
|
+
)
|
|
5067
5089
|
return AgentToolResult(
|
|
5068
5090
|
tool=tool_name,
|
|
5069
5091
|
ok=True,
|
|
@@ -5077,24 +5099,49 @@ class OpenClawWalletAdapter:
|
|
|
5077
5099
|
),
|
|
5078
5100
|
)
|
|
5079
5101
|
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5102
|
+
approved_preview = args.get("_approved_preview")
|
|
5103
|
+
execute_preview = None
|
|
5104
|
+
approval_payload = inspect_approval_token(
|
|
5105
|
+
approval_token,
|
|
5106
|
+
tool_name=tool_name,
|
|
5107
|
+
network=str(getattr(self.backend, "network", "unknown")),
|
|
5108
|
+
require_mainnet_confirmation=self._is_mainnet_for_backend(self.backend),
|
|
5084
5109
|
)
|
|
5110
|
+
approval_summary = approval_payload.get("binding", {}).get("summary")
|
|
5111
|
+
if not isinstance(approval_summary, dict):
|
|
5112
|
+
raise WalletBackendError(
|
|
5113
|
+
"approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
|
|
5114
|
+
)
|
|
5115
|
+
approval_summary_copy = dict(approval_summary)
|
|
5116
|
+
if isinstance(approval_summary_copy.get("_preview_digest"), str):
|
|
5117
|
+
if not isinstance(approved_preview, dict):
|
|
5118
|
+
raise WalletBackendError(
|
|
5119
|
+
f"Approved {action_label} preview payload is required for execute mode. Generate a new preview and approval before execute."
|
|
5120
|
+
)
|
|
5121
|
+
if preview_payload_digest(approved_preview) != approval_summary_copy["_preview_digest"]:
|
|
5122
|
+
raise WalletBackendError(
|
|
5123
|
+
"approved preview payload does not match the approval token. Generate a new preview and approval before execute."
|
|
5124
|
+
)
|
|
5125
|
+
execute_preview = dict(approved_preview)
|
|
5126
|
+
else:
|
|
5127
|
+
execute_preview = await preview_method(
|
|
5128
|
+
market=market.strip(),
|
|
5129
|
+
reserve=reserve.strip(),
|
|
5130
|
+
amount_ui=amount_ui.strip(),
|
|
5131
|
+
obligation_address=obligation_address.strip() if isinstance(obligation_address, str) and obligation_address.strip() else None,
|
|
5132
|
+
)
|
|
5085
5133
|
self._require_execute_approval(
|
|
5086
5134
|
approval_token=approval_token,
|
|
5087
5135
|
tool_name=tool_name,
|
|
5088
|
-
summary=
|
|
5089
|
-
action_label=action_label,
|
|
5090
|
-
payload=execute_preview,
|
|
5091
|
-
),
|
|
5136
|
+
summary=approval_summary_copy,
|
|
5092
5137
|
action_label=action_label,
|
|
5093
5138
|
)
|
|
5094
5139
|
result = await execute_method(
|
|
5095
5140
|
market=market.strip(),
|
|
5096
5141
|
reserve=reserve.strip(),
|
|
5097
5142
|
amount_ui=amount_ui.strip(),
|
|
5143
|
+
obligation_address=obligation_address.strip() if isinstance(obligation_address, str) and obligation_address.strip() else None,
|
|
5144
|
+
approved_preview=execute_preview,
|
|
5098
5145
|
)
|
|
5099
5146
|
return AgentToolResult(
|
|
5100
5147
|
tool=tool_name,
|
|
@@ -10,6 +10,8 @@ from agent_wallet.config import settings
|
|
|
10
10
|
from agent_wallet.exceptions import ProviderError
|
|
11
11
|
from agent_wallet.http_client import get_client
|
|
12
12
|
|
|
13
|
+
JUPITER_SWAP_FALLBACK_PRIORITY_MAX_LAMPORTS = 2_000_000
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
def _headers() -> dict[str, str]:
|
|
15
17
|
headers = {"Accept": "application/json"}
|
|
@@ -91,6 +93,32 @@ def _direct_jupiter_enabled() -> bool:
|
|
|
91
93
|
return bool(settings.jupiter_api_key.strip())
|
|
92
94
|
|
|
93
95
|
|
|
96
|
+
def _swap_fallback_prioritization_fee() -> dict[str, Any]:
|
|
97
|
+
return {
|
|
98
|
+
"priorityLevelWithMaxLamports": {
|
|
99
|
+
"priorityLevel": "veryHigh",
|
|
100
|
+
"maxLamports": JUPITER_SWAP_FALLBACK_PRIORITY_MAX_LAMPORTS,
|
|
101
|
+
"global": False,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _swap_fallback_build_body(
|
|
107
|
+
*,
|
|
108
|
+
user_public_key: str,
|
|
109
|
+
quote_response: dict[str, Any],
|
|
110
|
+
wrap_and_unwrap_sol: bool,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
return {
|
|
113
|
+
"userPublicKey": user_public_key,
|
|
114
|
+
"quoteResponse": quote_response,
|
|
115
|
+
"wrapAndUnwrapSol": wrap_and_unwrap_sol,
|
|
116
|
+
"dynamicComputeUnitLimit": True,
|
|
117
|
+
"dynamicSlippage": True,
|
|
118
|
+
"prioritizationFeeLamports": _swap_fallback_prioritization_fee(),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
94
122
|
def _swap_v2_base_url() -> str:
|
|
95
123
|
return os.getenv(
|
|
96
124
|
"JUPITER_SWAP_V2_API_BASE_URL",
|
|
@@ -456,13 +484,11 @@ async def _build_swap_direct(
|
|
|
456
484
|
) -> dict[str, Any]:
|
|
457
485
|
"""Build a swap transaction directly via Jupiter API."""
|
|
458
486
|
client = get_client()
|
|
459
|
-
body =
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
"prioritizationFeeLamports": "auto",
|
|
465
|
-
}
|
|
487
|
+
body = _swap_fallback_build_body(
|
|
488
|
+
user_public_key=user_public_key,
|
|
489
|
+
quote_response=quote_response,
|
|
490
|
+
wrap_and_unwrap_sol=wrap_and_unwrap_sol,
|
|
491
|
+
)
|
|
466
492
|
response = await client.post(
|
|
467
493
|
f"{settings.jupiter_api_base_url.rstrip('/')}/swap",
|
|
468
494
|
json=body,
|
|
@@ -483,13 +509,11 @@ async def _build_swap_via_gateway(
|
|
|
483
509
|
wrap_and_unwrap_sol: bool = True,
|
|
484
510
|
) -> dict[str, Any]:
|
|
485
511
|
"""Build a swap transaction via provider gateway (uses API key)."""
|
|
486
|
-
body =
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"prioritizationFeeLamports": "auto",
|
|
492
|
-
}
|
|
512
|
+
body = _swap_fallback_build_body(
|
|
513
|
+
user_public_key=user_public_key,
|
|
514
|
+
quote_response=quote_response,
|
|
515
|
+
wrap_and_unwrap_sol=wrap_and_unwrap_sol,
|
|
516
|
+
)
|
|
493
517
|
status_code, payload = await _gateway_post("swap", body=body)
|
|
494
518
|
if status_code != 200:
|
|
495
519
|
error_msg = payload if isinstance(payload, str) else json.dumps(payload)
|
|
@@ -8,6 +8,8 @@ from agent_wallet.config import settings
|
|
|
8
8
|
from agent_wallet.exceptions import ProviderError
|
|
9
9
|
from agent_wallet.http_client import get_client
|
|
10
10
|
|
|
11
|
+
KAMINO_BUILD_TIMEOUT_SECONDS = 20.0
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
def _normalized_api_base() -> str:
|
|
13
15
|
return settings.kamino_api_base_url.rstrip("/")
|
|
@@ -140,6 +142,7 @@ async def build_lend_deposit_transaction(
|
|
|
140
142
|
"reserve": reserve,
|
|
141
143
|
"amount": amount_ui,
|
|
142
144
|
},
|
|
145
|
+
timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
|
|
143
146
|
)
|
|
144
147
|
if response.status_code != 200:
|
|
145
148
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
@@ -163,6 +166,7 @@ async def build_lend_withdraw_transaction(
|
|
|
163
166
|
"reserve": reserve,
|
|
164
167
|
"amount": amount_ui,
|
|
165
168
|
},
|
|
169
|
+
timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
|
|
166
170
|
)
|
|
167
171
|
if response.status_code != 200:
|
|
168
172
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
@@ -186,6 +190,7 @@ async def build_lend_borrow_transaction(
|
|
|
186
190
|
"reserve": reserve,
|
|
187
191
|
"amount": amount_ui,
|
|
188
192
|
},
|
|
193
|
+
timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
|
|
189
194
|
)
|
|
190
195
|
if response.status_code != 200:
|
|
191
196
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
@@ -209,6 +214,7 @@ async def build_lend_repay_transaction(
|
|
|
209
214
|
"reserve": reserve,
|
|
210
215
|
"amount": amount_ui,
|
|
211
216
|
},
|
|
217
|
+
timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
|
|
212
218
|
)
|
|
213
219
|
if response.status_code != 200:
|
|
214
220
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
@@ -718,6 +718,7 @@ def verify_provider_kamino_lend_transaction(
|
|
|
718
718
|
market_address: str,
|
|
719
719
|
reserve_address: str,
|
|
720
720
|
action: str,
|
|
721
|
+
obligation_address: str | None = None,
|
|
721
722
|
loaded_addresses: list[str] | None = None,
|
|
722
723
|
) -> dict[str, Any]:
|
|
723
724
|
binding = _assert_basic_wallet_binding(
|
|
@@ -734,6 +735,10 @@ def verify_provider_kamino_lend_transaction(
|
|
|
734
735
|
raise WalletBackendError(
|
|
735
736
|
f"{action} transaction does not reference the expected Kamino reserve."
|
|
736
737
|
)
|
|
738
|
+
if obligation_address and obligation_address not in keys:
|
|
739
|
+
raise WalletBackendError(
|
|
740
|
+
f"{action} transaction does not reference the expected Kamino obligation."
|
|
741
|
+
)
|
|
737
742
|
program_ids = _program_ids(message, loaded_addresses)
|
|
738
743
|
unknown_program_ids = _assert_program_allowlist(
|
|
739
744
|
program_ids,
|
|
@@ -764,6 +769,7 @@ def verify_provider_kamino_lend_transaction(
|
|
|
764
769
|
"instruction_count": len(_compiled_instructions(message)),
|
|
765
770
|
"market_address": market_address,
|
|
766
771
|
"reserve_address": reserve_address,
|
|
772
|
+
"obligation_address": obligation_address,
|
|
767
773
|
"action": action,
|
|
768
774
|
"verified": True,
|
|
769
775
|
}
|
|
@@ -525,6 +525,7 @@ class AgentWalletBackend(ABC):
|
|
|
525
525
|
market: str,
|
|
526
526
|
reserve: str,
|
|
527
527
|
amount_ui: str,
|
|
528
|
+
obligation_address: str | None = None,
|
|
528
529
|
) -> dict[str, Any]:
|
|
529
530
|
raise WalletBackendError(f"{self.name} does not support Kamino deposit previews.")
|
|
530
531
|
|
|
@@ -533,6 +534,8 @@ class AgentWalletBackend(ABC):
|
|
|
533
534
|
market: str,
|
|
534
535
|
reserve: str,
|
|
535
536
|
amount_ui: str,
|
|
537
|
+
obligation_address: str | None = None,
|
|
538
|
+
approved_preview: dict[str, Any] | None = None,
|
|
536
539
|
) -> dict[str, Any]:
|
|
537
540
|
raise WalletBackendError(f"{self.name} does not support Kamino deposit preparation.")
|
|
538
541
|
|
|
@@ -541,6 +544,8 @@ class AgentWalletBackend(ABC):
|
|
|
541
544
|
market: str,
|
|
542
545
|
reserve: str,
|
|
543
546
|
amount_ui: str,
|
|
547
|
+
obligation_address: str | None = None,
|
|
548
|
+
approved_preview: dict[str, Any] | None = None,
|
|
544
549
|
) -> dict[str, Any]:
|
|
545
550
|
raise WalletBackendError(f"{self.name} does not support Kamino deposits.")
|
|
546
551
|
|
|
@@ -549,6 +554,7 @@ class AgentWalletBackend(ABC):
|
|
|
549
554
|
market: str,
|
|
550
555
|
reserve: str,
|
|
551
556
|
amount_ui: str,
|
|
557
|
+
obligation_address: str | None = None,
|
|
552
558
|
) -> dict[str, Any]:
|
|
553
559
|
raise WalletBackendError(f"{self.name} does not support Kamino withdraw previews.")
|
|
554
560
|
|
|
@@ -557,6 +563,8 @@ class AgentWalletBackend(ABC):
|
|
|
557
563
|
market: str,
|
|
558
564
|
reserve: str,
|
|
559
565
|
amount_ui: str,
|
|
566
|
+
obligation_address: str | None = None,
|
|
567
|
+
approved_preview: dict[str, Any] | None = None,
|
|
560
568
|
) -> dict[str, Any]:
|
|
561
569
|
raise WalletBackendError(f"{self.name} does not support Kamino withdraw preparation.")
|
|
562
570
|
|
|
@@ -565,6 +573,8 @@ class AgentWalletBackend(ABC):
|
|
|
565
573
|
market: str,
|
|
566
574
|
reserve: str,
|
|
567
575
|
amount_ui: str,
|
|
576
|
+
obligation_address: str | None = None,
|
|
577
|
+
approved_preview: dict[str, Any] | None = None,
|
|
568
578
|
) -> dict[str, Any]:
|
|
569
579
|
raise WalletBackendError(f"{self.name} does not support Kamino withdraws.")
|
|
570
580
|
|
|
@@ -573,6 +583,7 @@ class AgentWalletBackend(ABC):
|
|
|
573
583
|
market: str,
|
|
574
584
|
reserve: str,
|
|
575
585
|
amount_ui: str,
|
|
586
|
+
obligation_address: str | None = None,
|
|
576
587
|
) -> dict[str, Any]:
|
|
577
588
|
raise WalletBackendError(f"{self.name} does not support Kamino borrow previews.")
|
|
578
589
|
|
|
@@ -581,6 +592,8 @@ class AgentWalletBackend(ABC):
|
|
|
581
592
|
market: str,
|
|
582
593
|
reserve: str,
|
|
583
594
|
amount_ui: str,
|
|
595
|
+
obligation_address: str | None = None,
|
|
596
|
+
approved_preview: dict[str, Any] | None = None,
|
|
584
597
|
) -> dict[str, Any]:
|
|
585
598
|
raise WalletBackendError(f"{self.name} does not support Kamino borrow preparation.")
|
|
586
599
|
|
|
@@ -589,6 +602,8 @@ class AgentWalletBackend(ABC):
|
|
|
589
602
|
market: str,
|
|
590
603
|
reserve: str,
|
|
591
604
|
amount_ui: str,
|
|
605
|
+
obligation_address: str | None = None,
|
|
606
|
+
approved_preview: dict[str, Any] | None = None,
|
|
592
607
|
) -> dict[str, Any]:
|
|
593
608
|
raise WalletBackendError(f"{self.name} does not support Kamino borrows.")
|
|
594
609
|
|
|
@@ -597,6 +612,7 @@ class AgentWalletBackend(ABC):
|
|
|
597
612
|
market: str,
|
|
598
613
|
reserve: str,
|
|
599
614
|
amount_ui: str,
|
|
615
|
+
obligation_address: str | None = None,
|
|
600
616
|
) -> dict[str, Any]:
|
|
601
617
|
raise WalletBackendError(f"{self.name} does not support Kamino repay previews.")
|
|
602
618
|
|
|
@@ -605,6 +621,8 @@ class AgentWalletBackend(ABC):
|
|
|
605
621
|
market: str,
|
|
606
622
|
reserve: str,
|
|
607
623
|
amount_ui: str,
|
|
624
|
+
obligation_address: str | None = None,
|
|
625
|
+
approved_preview: dict[str, Any] | None = None,
|
|
608
626
|
) -> dict[str, Any]:
|
|
609
627
|
raise WalletBackendError(f"{self.name} does not support Kamino repay preparation.")
|
|
610
628
|
|
|
@@ -613,6 +631,8 @@ class AgentWalletBackend(ABC):
|
|
|
613
631
|
market: str,
|
|
614
632
|
reserve: str,
|
|
615
633
|
amount_ui: str,
|
|
634
|
+
obligation_address: str | None = None,
|
|
635
|
+
approved_preview: dict[str, Any] | None = None,
|
|
616
636
|
) -> dict[str, Any]:
|
|
617
637
|
raise WalletBackendError(f"{self.name} does not support Kamino repays.")
|
|
618
638
|
|
|
@@ -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")
|
|
@@ -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`
|