@agentlayer.tech/wallet 0.1.65 → 0.1.67
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/index.ts +82 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +7 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/VERSION +1 -1
- package/agent-wallet/README.md +6 -0
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +485 -4
- package/agent-wallet/agent_wallet/providers/kamino.py +169 -3
- package/agent-wallet/agent_wallet/providers/x402.py +229 -148
- package/agent-wallet/agent_wallet/transaction_policy.py +60 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +124 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +563 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +16 -0
- package/agent-wallet/openclaw.plugin.json +2 -2
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/install_openclaw_local_config.py +6 -0
- package/agent-wallet/skills/wallet-operator/SKILL.md +6 -4
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/server.py +52 -3
- package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/install-from-github.sh +6 -0
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +126 -19
|
@@ -29,6 +29,7 @@ from agent_wallet.solana_tx import (
|
|
|
29
29
|
from agent_wallet.transaction_policy import (
|
|
30
30
|
verify_provider_bags_transaction,
|
|
31
31
|
verify_provider_flash_transaction,
|
|
32
|
+
verify_provider_kamino_earn_transaction,
|
|
32
33
|
verify_provider_kamino_lend_transaction,
|
|
33
34
|
verify_provider_swap_simulation_result,
|
|
34
35
|
verify_provider_swap_transaction,
|
|
@@ -51,6 +52,9 @@ STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111"
|
|
|
51
52
|
SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS = 300
|
|
52
53
|
SOLANA_SWAP_INTENT_DEFAULT_MAX_FEE_LAMPORTS = 6_000_000
|
|
53
54
|
KAMINO_OPEN_POSITIONS_SCAN_CONCURRENCY = 6
|
|
55
|
+
# The Earn vaults catalog has no batch APY endpoint, so metrics are fetched
|
|
56
|
+
# per vault; cap the fan-out so one discovery call stays bounded.
|
|
57
|
+
KAMINO_EARN_METRICS_FETCH_LIMIT = 20
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
def _load_signing_key():
|
|
@@ -2078,6 +2082,274 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
2078
2082
|
prepared = await self._prepare_flash_trade_close_position_from_preview(preview)
|
|
2079
2083
|
return await self._execute_prepared_flash_trade_transaction(prepared)
|
|
2080
2084
|
|
|
2085
|
+
async def get_kamino_portfolio(self, user: str | None = None) -> dict[str, Any]:
|
|
2086
|
+
self._require_mainnet_kamino("Kamino portfolio")
|
|
2087
|
+
wallet_address = user or self.address
|
|
2088
|
+
if not wallet_address:
|
|
2089
|
+
raise WalletBackendError("A wallet address is required for Kamino portfolio lookup.")
|
|
2090
|
+
wallet_address = validate_solana_address(wallet_address)
|
|
2091
|
+
data = await kamino.fetch_portfolio(user=wallet_address)
|
|
2092
|
+
|
|
2093
|
+
sections = data.get("sections") if isinstance(data.get("sections"), dict) else {}
|
|
2094
|
+
|
|
2095
|
+
def _section_items(key: str) -> list[Any]:
|
|
2096
|
+
value = data.get(key)
|
|
2097
|
+
return value if isinstance(value, list) else []
|
|
2098
|
+
|
|
2099
|
+
lending = _section_items("lending")
|
|
2100
|
+
multiply = _section_items("multiply")
|
|
2101
|
+
leverage = _section_items("leverage")
|
|
2102
|
+
liquidity = _section_items("liquidity")
|
|
2103
|
+
earn = _section_items("earn")
|
|
2104
|
+
private_credit = _section_items("privateCredit")
|
|
2105
|
+
staking = _section_items("staking")
|
|
2106
|
+
|
|
2107
|
+
return {
|
|
2108
|
+
"chain": "solana",
|
|
2109
|
+
"network": self.network,
|
|
2110
|
+
"user": wallet_address,
|
|
2111
|
+
"timestamp": data.get("timestamp"),
|
|
2112
|
+
"sections": sections,
|
|
2113
|
+
"lending_count": len(lending),
|
|
2114
|
+
"multiply_count": len(multiply),
|
|
2115
|
+
"leverage_count": len(leverage),
|
|
2116
|
+
"liquidity_count": len(liquidity),
|
|
2117
|
+
"earn_count": len(earn),
|
|
2118
|
+
"private_credit_count": len(private_credit),
|
|
2119
|
+
"staking_count": len(staking),
|
|
2120
|
+
"position_count": (
|
|
2121
|
+
len(lending)
|
|
2122
|
+
+ len(multiply)
|
|
2123
|
+
+ len(leverage)
|
|
2124
|
+
+ len(liquidity)
|
|
2125
|
+
+ len(earn)
|
|
2126
|
+
+ len(private_credit)
|
|
2127
|
+
+ len(staking)
|
|
2128
|
+
),
|
|
2129
|
+
"lending": lending,
|
|
2130
|
+
"multiply": multiply,
|
|
2131
|
+
"leverage": leverage,
|
|
2132
|
+
"liquidity": liquidity,
|
|
2133
|
+
"earn": earn,
|
|
2134
|
+
"private_credit": private_credit,
|
|
2135
|
+
"staking": staking,
|
|
2136
|
+
"raw": data,
|
|
2137
|
+
"source": "kamino",
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
@staticmethod
|
|
2141
|
+
def _kamino_vault_summary(entry: Any) -> dict[str, Any] | None:
|
|
2142
|
+
"""Compact one raw /kvaults entry (address + full on-chain state dump)
|
|
2143
|
+
into the fields an agent needs for discovery. The raw state is ~2 KB
|
|
2144
|
+
per vault and carries no yield data, so passing it through verbatim
|
|
2145
|
+
only burns agent context."""
|
|
2146
|
+
if not isinstance(entry, dict):
|
|
2147
|
+
return None
|
|
2148
|
+
address = _kamino_entry_address(entry, "address")
|
|
2149
|
+
state = entry.get("state") if isinstance(entry.get("state"), dict) else {}
|
|
2150
|
+
if not address:
|
|
2151
|
+
return None
|
|
2152
|
+
|
|
2153
|
+
def _int_or_none(value: Any) -> int | None:
|
|
2154
|
+
try:
|
|
2155
|
+
return int(str(value))
|
|
2156
|
+
except (TypeError, ValueError):
|
|
2157
|
+
return None
|
|
2158
|
+
|
|
2159
|
+
return {
|
|
2160
|
+
"vault": address,
|
|
2161
|
+
"name": str(state.get("name") or "").strip() or None,
|
|
2162
|
+
"token_mint": str(state.get("tokenMint") or "").strip() or None,
|
|
2163
|
+
"shares_mint": str(state.get("sharesMint") or "").strip() or None,
|
|
2164
|
+
"management_fee_bps": _int_or_none(state.get("managementFeeBps")),
|
|
2165
|
+
"performance_fee_bps": _int_or_none(state.get("performanceFeeBps")),
|
|
2166
|
+
"min_deposit_amount": str(state.get("minDepositAmount") or "0"),
|
|
2167
|
+
"creation_timestamp": _int_or_none(state.get("creationTimestamp")),
|
|
2168
|
+
"prev_aum_raw": str(state.get("prevAum") or "0"),
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
@staticmethod
|
|
2172
|
+
def _kamino_vault_metrics_summary(metrics: dict[str, Any]) -> dict[str, Any]:
|
|
2173
|
+
return {
|
|
2174
|
+
"apy": metrics.get("apy"),
|
|
2175
|
+
"apy_actual": metrics.get("apyActual"),
|
|
2176
|
+
"apy_7d": metrics.get("apy7d"),
|
|
2177
|
+
"apy_30d": metrics.get("apy30d"),
|
|
2178
|
+
"apy_farm_rewards": metrics.get("apyFarmRewards"),
|
|
2179
|
+
"tokens_invested_usd": metrics.get("tokensInvestedUsd"),
|
|
2180
|
+
"tokens_available_usd": metrics.get("tokensAvailableUsd"),
|
|
2181
|
+
"share_price": metrics.get("sharePrice"),
|
|
2182
|
+
"number_of_holders": metrics.get("numberOfHolders"),
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
async def get_kamino_vaults(
|
|
2186
|
+
self,
|
|
2187
|
+
vault_address: str | None = None,
|
|
2188
|
+
token_mint: str | None = None,
|
|
2189
|
+
include_metrics: bool = False,
|
|
2190
|
+
limit: int | None = None,
|
|
2191
|
+
) -> dict[str, Any]:
|
|
2192
|
+
self._require_mainnet_kamino("Kamino Earn")
|
|
2193
|
+
|
|
2194
|
+
if isinstance(vault_address, str) and vault_address.strip():
|
|
2195
|
+
vault_address = validate_solana_address(vault_address.strip())
|
|
2196
|
+
data = await kamino.fetch_earn_vaults()
|
|
2197
|
+
entries = data.get("vaults") if isinstance(data.get("vaults"), list) else []
|
|
2198
|
+
summary = next(
|
|
2199
|
+
(
|
|
2200
|
+
candidate
|
|
2201
|
+
for entry in entries
|
|
2202
|
+
if (candidate := self._kamino_vault_summary(entry))
|
|
2203
|
+
and candidate["vault"] == vault_address
|
|
2204
|
+
),
|
|
2205
|
+
None,
|
|
2206
|
+
)
|
|
2207
|
+
if summary is None:
|
|
2208
|
+
raise WalletBackendError(f"No Kamino Earn vault matched: {vault_address}")
|
|
2209
|
+
metrics = await kamino.fetch_earn_vault_metrics(
|
|
2210
|
+
vault=vault_address, network=self.network
|
|
2211
|
+
)
|
|
2212
|
+
summary["metrics"] = self._kamino_vault_metrics_summary(metrics)
|
|
2213
|
+
return {
|
|
2214
|
+
"chain": "solana",
|
|
2215
|
+
"network": self.network,
|
|
2216
|
+
"vault_count": 1,
|
|
2217
|
+
"vaults": [summary],
|
|
2218
|
+
"source": "kamino",
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
max_limit = 100
|
|
2222
|
+
effective_limit = 50
|
|
2223
|
+
if limit is not None:
|
|
2224
|
+
if not isinstance(limit, int) or limit <= 0:
|
|
2225
|
+
raise WalletBackendError("limit must be a positive integer.")
|
|
2226
|
+
effective_limit = min(limit, max_limit)
|
|
2227
|
+
|
|
2228
|
+
mint_filter = None
|
|
2229
|
+
if isinstance(token_mint, str) and token_mint.strip():
|
|
2230
|
+
mint_filter = validate_solana_address(token_mint.strip())
|
|
2231
|
+
|
|
2232
|
+
data = await kamino.fetch_earn_vaults()
|
|
2233
|
+
entries = data.get("vaults") if isinstance(data.get("vaults"), list) else []
|
|
2234
|
+
summaries = [
|
|
2235
|
+
summary
|
|
2236
|
+
for entry in entries
|
|
2237
|
+
if (summary := self._kamino_vault_summary(entry)) is not None
|
|
2238
|
+
]
|
|
2239
|
+
total_count = len(summaries)
|
|
2240
|
+
if mint_filter:
|
|
2241
|
+
summaries = [item for item in summaries if item["token_mint"] == mint_filter]
|
|
2242
|
+
|
|
2243
|
+
def _aum_key(item: dict[str, Any]) -> float:
|
|
2244
|
+
# prevAum is a decimal string on active vaults (e.g.
|
|
2245
|
+
# "21724442402923.2067…"); parsing it as int would throw on the
|
|
2246
|
+
# fractional part and sink every production vault to rank 0.
|
|
2247
|
+
try:
|
|
2248
|
+
return float(item["prev_aum_raw"])
|
|
2249
|
+
except (TypeError, ValueError):
|
|
2250
|
+
return 0.0
|
|
2251
|
+
|
|
2252
|
+
# prevAum is in raw token units, so it is only a rough pre-rank across
|
|
2253
|
+
# mixed mints; with a token_mint filter it is directly comparable.
|
|
2254
|
+
summaries.sort(key=_aum_key, reverse=True)
|
|
2255
|
+
|
|
2256
|
+
metrics_errors: list[dict[str, Any]] = []
|
|
2257
|
+
if include_metrics:
|
|
2258
|
+
candidates = summaries[: min(effective_limit, KAMINO_EARN_METRICS_FETCH_LIMIT)]
|
|
2259
|
+
semaphore = asyncio.Semaphore(KAMINO_OPEN_POSITIONS_SCAN_CONCURRENCY)
|
|
2260
|
+
|
|
2261
|
+
async def _attach_metrics(item: dict[str, Any]) -> None:
|
|
2262
|
+
try:
|
|
2263
|
+
async with semaphore:
|
|
2264
|
+
metrics = await kamino.fetch_earn_vault_metrics(
|
|
2265
|
+
vault=item["vault"], network=self.network
|
|
2266
|
+
)
|
|
2267
|
+
except (ProviderError, WalletBackendError) as exc:
|
|
2268
|
+
metrics_errors.append({"vault": item["vault"], "error": str(exc)})
|
|
2269
|
+
return
|
|
2270
|
+
item["metrics"] = self._kamino_vault_metrics_summary(metrics)
|
|
2271
|
+
|
|
2272
|
+
await asyncio.gather(*[_attach_metrics(item) for item in candidates])
|
|
2273
|
+
|
|
2274
|
+
def _apy_key(item: dict[str, Any]) -> float:
|
|
2275
|
+
metrics = item.get("metrics")
|
|
2276
|
+
try:
|
|
2277
|
+
return float((metrics or {}).get("apy") or 0)
|
|
2278
|
+
except (TypeError, ValueError):
|
|
2279
|
+
return 0.0
|
|
2280
|
+
|
|
2281
|
+
candidates.sort(key=_apy_key, reverse=True)
|
|
2282
|
+
summaries = candidates
|
|
2283
|
+
|
|
2284
|
+
vaults = summaries[:effective_limit]
|
|
2285
|
+
result: dict[str, Any] = {
|
|
2286
|
+
"chain": "solana",
|
|
2287
|
+
"network": self.network,
|
|
2288
|
+
"total_vault_count": total_count,
|
|
2289
|
+
"token_mint_filter": mint_filter,
|
|
2290
|
+
"include_metrics": bool(include_metrics),
|
|
2291
|
+
"vault_count": len(vaults),
|
|
2292
|
+
"vaults": vaults,
|
|
2293
|
+
"source": "kamino",
|
|
2294
|
+
}
|
|
2295
|
+
if metrics_errors:
|
|
2296
|
+
result["metrics_errors"] = metrics_errors
|
|
2297
|
+
return result
|
|
2298
|
+
|
|
2299
|
+
async def get_kamino_earn_positions(self, user: str | None = None) -> dict[str, Any]:
|
|
2300
|
+
self._require_mainnet_kamino("Kamino Earn")
|
|
2301
|
+
wallet_address = user or self.address
|
|
2302
|
+
if not wallet_address:
|
|
2303
|
+
raise WalletBackendError("A wallet address is required for Kamino Earn position lookup.")
|
|
2304
|
+
wallet_address = validate_solana_address(wallet_address)
|
|
2305
|
+
data = await kamino.fetch_earn_user_positions(
|
|
2306
|
+
user=wallet_address,
|
|
2307
|
+
network=self.network,
|
|
2308
|
+
)
|
|
2309
|
+
positions = data.get("positions")
|
|
2310
|
+
if not isinstance(positions, list):
|
|
2311
|
+
positions = []
|
|
2312
|
+
return {
|
|
2313
|
+
"chain": "solana",
|
|
2314
|
+
"network": self.network,
|
|
2315
|
+
"user": wallet_address,
|
|
2316
|
+
"position_count": len(positions),
|
|
2317
|
+
"positions": positions,
|
|
2318
|
+
"raw": data,
|
|
2319
|
+
"source": "kamino",
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
async def get_kamino_liquidity_positions(self, user: str | None = None) -> dict[str, Any]:
|
|
2323
|
+
self._require_mainnet_kamino("Kamino Liquidity")
|
|
2324
|
+
portfolio = await self.get_kamino_portfolio(user=user)
|
|
2325
|
+
liquidity = portfolio.get("liquidity")
|
|
2326
|
+
if not isinstance(liquidity, list):
|
|
2327
|
+
liquidity = []
|
|
2328
|
+
sections = portfolio.get("sections")
|
|
2329
|
+
liquidity_section = sections.get("liquidity") if isinstance(sections, dict) else {}
|
|
2330
|
+
return {
|
|
2331
|
+
"chain": "solana",
|
|
2332
|
+
"network": self.network,
|
|
2333
|
+
"user": portfolio["user"],
|
|
2334
|
+
"timestamp": portfolio.get("timestamp"),
|
|
2335
|
+
"positions_indexed": bool(liquidity_section.get("indexed"))
|
|
2336
|
+
if isinstance(liquidity_section, dict)
|
|
2337
|
+
else None,
|
|
2338
|
+
"positions_refreshed_on": liquidity_section.get("positionsRefreshedOn")
|
|
2339
|
+
if isinstance(liquidity_section, dict)
|
|
2340
|
+
else None,
|
|
2341
|
+
"prices_refreshed_on": liquidity_section.get("pricesRefreshedOn")
|
|
2342
|
+
if isinstance(liquidity_section, dict)
|
|
2343
|
+
else None,
|
|
2344
|
+
"errors": liquidity_section.get("errors")
|
|
2345
|
+
if isinstance(liquidity_section, dict) and isinstance(liquidity_section.get("errors"), list)
|
|
2346
|
+
else [],
|
|
2347
|
+
"position_count": len(liquidity),
|
|
2348
|
+
"positions": liquidity,
|
|
2349
|
+
"raw": portfolio.get("raw"),
|
|
2350
|
+
"source": "kamino+portfolio",
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2081
2353
|
async def get_kamino_lend_markets(self) -> dict[str, Any]:
|
|
2082
2354
|
self._require_mainnet_kamino("Kamino lending")
|
|
2083
2355
|
data = await kamino.fetch_lend_markets()
|
|
@@ -3230,6 +3502,30 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3230
3502
|
return item
|
|
3231
3503
|
return None
|
|
3232
3504
|
|
|
3505
|
+
def _find_kamino_vault_entry(
|
|
3506
|
+
self,
|
|
3507
|
+
*,
|
|
3508
|
+
vaults: list[Any],
|
|
3509
|
+
kvault: str,
|
|
3510
|
+
) -> dict[str, Any] | None:
|
|
3511
|
+
for item in vaults:
|
|
3512
|
+
if _kamino_entry_address(item, "address", "kvault", "vault", "pubkey") == kvault:
|
|
3513
|
+
return item
|
|
3514
|
+
return None
|
|
3515
|
+
|
|
3516
|
+
def _find_kamino_earn_position_entry(
|
|
3517
|
+
self,
|
|
3518
|
+
*,
|
|
3519
|
+
positions: list[Any],
|
|
3520
|
+
kvault: str,
|
|
3521
|
+
) -> dict[str, Any] | None:
|
|
3522
|
+
for item in positions:
|
|
3523
|
+
if not isinstance(item, dict):
|
|
3524
|
+
continue
|
|
3525
|
+
if _kamino_entry_address(item, "vaultAddress", "kvault", "vault", "address", "pubkey") == kvault:
|
|
3526
|
+
return item
|
|
3527
|
+
return None
|
|
3528
|
+
|
|
3233
3529
|
def _find_kamino_obligation_matches(
|
|
3234
3530
|
self,
|
|
3235
3531
|
*,
|
|
@@ -3387,6 +3683,97 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3387
3683
|
"source": "kamino",
|
|
3388
3684
|
}
|
|
3389
3685
|
|
|
3686
|
+
async def _prepare_kamino_earn_transaction(
|
|
3687
|
+
self,
|
|
3688
|
+
*,
|
|
3689
|
+
transaction_base64: str,
|
|
3690
|
+
action: str,
|
|
3691
|
+
kvault: str,
|
|
3692
|
+
amount_ui: str,
|
|
3693
|
+
vault_token_mint: str | None = None,
|
|
3694
|
+
) -> dict[str, Any]:
|
|
3695
|
+
if not self.signer:
|
|
3696
|
+
raise WalletBackendError("Solana signer is not configured.")
|
|
3697
|
+
try:
|
|
3698
|
+
from solders.transaction import VersionedTransaction
|
|
3699
|
+
except ImportError as exc:
|
|
3700
|
+
raise WalletBackendError(
|
|
3701
|
+
"solana and solders packages are required for Kamino transaction signing."
|
|
3702
|
+
) from exc
|
|
3703
|
+
owner = await self.get_address()
|
|
3704
|
+
unsigned_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_base64))
|
|
3705
|
+
loaded_addresses = await self._resolve_versioned_message_lookup_addresses(
|
|
3706
|
+
unsigned_transaction.message
|
|
3707
|
+
)
|
|
3708
|
+
verification = verify_provider_kamino_earn_transaction(
|
|
3709
|
+
unsigned_transaction.message,
|
|
3710
|
+
wallet_address=str(owner),
|
|
3711
|
+
vault_address=kvault,
|
|
3712
|
+
action=f"Kamino Earn {action}",
|
|
3713
|
+
vault_token_mint=vault_token_mint,
|
|
3714
|
+
loaded_addresses=loaded_addresses,
|
|
3715
|
+
)
|
|
3716
|
+
signed_transaction_base64 = await self._sign_versioned_provider_transaction(
|
|
3717
|
+
transaction_base64=transaction_base64,
|
|
3718
|
+
wallet_signer_index=int(verification.get("wallet_signer_index") or 0),
|
|
3719
|
+
)
|
|
3720
|
+
simulation_value: dict[str, Any] | None = None
|
|
3721
|
+
kamino_safety: dict[str, Any]
|
|
3722
|
+
try:
|
|
3723
|
+
simulation = await solana_rpc.simulate_transaction(
|
|
3724
|
+
transaction_base64=signed_transaction_base64,
|
|
3725
|
+
rpc_url=self.rpc_urls,
|
|
3726
|
+
commitment=self.commitment,
|
|
3727
|
+
)
|
|
3728
|
+
simulation_value = (
|
|
3729
|
+
simulation.get("value") if isinstance(simulation.get("value"), dict) else {}
|
|
3730
|
+
)
|
|
3731
|
+
if isinstance(simulation_value, dict) and simulation_value.get("err") is not None:
|
|
3732
|
+
raise WalletBackendError(
|
|
3733
|
+
f"Kamino Earn {action} transaction simulation failed.",
|
|
3734
|
+
code="kamino_simulation_failed",
|
|
3735
|
+
details={
|
|
3736
|
+
"simulation": simulation_value,
|
|
3737
|
+
"action": action,
|
|
3738
|
+
"kvault": kvault,
|
|
3739
|
+
},
|
|
3740
|
+
)
|
|
3741
|
+
kamino_safety = {
|
|
3742
|
+
"verified": True,
|
|
3743
|
+
"simulation_unavailable": False,
|
|
3744
|
+
}
|
|
3745
|
+
except ProviderError as exc:
|
|
3746
|
+
kamino_safety = {
|
|
3747
|
+
"verified": False,
|
|
3748
|
+
"simulation_unavailable": True,
|
|
3749
|
+
"warning": (
|
|
3750
|
+
"Kamino simulation could not be completed via the configured Solana RPC. "
|
|
3751
|
+
"Proceeding with structural provider verification only."
|
|
3752
|
+
),
|
|
3753
|
+
"error": str(exc),
|
|
3754
|
+
}
|
|
3755
|
+
return {
|
|
3756
|
+
"chain": "solana",
|
|
3757
|
+
"network": self.network,
|
|
3758
|
+
"mode": "prepare",
|
|
3759
|
+
"asset_type": f"kamino-earn-{action}",
|
|
3760
|
+
"owner": owner,
|
|
3761
|
+
"kvault": kvault,
|
|
3762
|
+
"amount_ui": amount_ui,
|
|
3763
|
+
"vault_token_mint": vault_token_mint,
|
|
3764
|
+
"transaction_base64": signed_transaction_base64,
|
|
3765
|
+
"transaction_encoding": "base64",
|
|
3766
|
+
"transaction_format": "versioned",
|
|
3767
|
+
"signed": True,
|
|
3768
|
+
"broadcasted": False,
|
|
3769
|
+
"confirmed": False,
|
|
3770
|
+
"verification": verification,
|
|
3771
|
+
"simulation": simulation_value,
|
|
3772
|
+
"kamino_safety": kamino_safety,
|
|
3773
|
+
"sign_only": self.sign_only,
|
|
3774
|
+
"source": "kamino",
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3390
3777
|
def _kamino_preview_from_approved(
|
|
3391
3778
|
self,
|
|
3392
3779
|
approved_preview: dict[str, Any] | None,
|
|
@@ -3399,6 +3786,182 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
3399
3786
|
return None
|
|
3400
3787
|
return dict(approved_preview)
|
|
3401
3788
|
|
|
3789
|
+
async def preview_kamino_earn_deposit(
|
|
3790
|
+
self,
|
|
3791
|
+
kvault: str,
|
|
3792
|
+
amount_ui: str,
|
|
3793
|
+
) -> dict[str, Any]:
|
|
3794
|
+
self._require_mainnet_kamino("Kamino Earn")
|
|
3795
|
+
owner = await self.get_address()
|
|
3796
|
+
if not owner:
|
|
3797
|
+
raise WalletBackendError(
|
|
3798
|
+
"No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
|
|
3799
|
+
)
|
|
3800
|
+
kvault = validate_solana_address(kvault)
|
|
3801
|
+
amount_ui = _require_positive_decimal_string(amount_ui, field_name="amount_ui")
|
|
3802
|
+
vault_snapshot = await self.get_kamino_vaults()
|
|
3803
|
+
vault_entry = self._find_kamino_vault_entry(
|
|
3804
|
+
vaults=list(vault_snapshot["vaults"]),
|
|
3805
|
+
kvault=kvault,
|
|
3806
|
+
)
|
|
3807
|
+
if vault_entry is None:
|
|
3808
|
+
raise WalletBackendError("Requested Kamino Earn vault is not available.")
|
|
3809
|
+
return {
|
|
3810
|
+
"chain": "solana",
|
|
3811
|
+
"network": self.network,
|
|
3812
|
+
"mode": "preview",
|
|
3813
|
+
"asset_type": "kamino-earn-deposit",
|
|
3814
|
+
"owner": owner,
|
|
3815
|
+
"kvault": kvault,
|
|
3816
|
+
"amount_ui": amount_ui,
|
|
3817
|
+
"vault_info": vault_entry,
|
|
3818
|
+
"sign_only": self.sign_only,
|
|
3819
|
+
"can_send": self.get_capabilities().can_send_transaction,
|
|
3820
|
+
"source": "kamino",
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
async def prepare_kamino_earn_deposit(
|
|
3824
|
+
self,
|
|
3825
|
+
kvault: str,
|
|
3826
|
+
amount_ui: str,
|
|
3827
|
+
approved_preview: dict[str, Any] | None = None,
|
|
3828
|
+
) -> dict[str, Any]:
|
|
3829
|
+
preview = self._kamino_preview_from_approved(
|
|
3830
|
+
approved_preview,
|
|
3831
|
+
asset_type="kamino-earn-deposit",
|
|
3832
|
+
) or await self.preview_kamino_earn_deposit(
|
|
3833
|
+
kvault=kvault,
|
|
3834
|
+
amount_ui=amount_ui,
|
|
3835
|
+
)
|
|
3836
|
+
owner = str(preview["owner"])
|
|
3837
|
+
build = await kamino.build_earn_deposit_transaction(
|
|
3838
|
+
wallet=owner,
|
|
3839
|
+
kvault=str(preview["kvault"]),
|
|
3840
|
+
amount_ui=str(preview["amount_ui"]),
|
|
3841
|
+
)
|
|
3842
|
+
vault_info = preview.get("vault_info") if isinstance(preview.get("vault_info"), dict) else {}
|
|
3843
|
+
vault_state = vault_info.get("state") if isinstance(vault_info.get("state"), dict) else {}
|
|
3844
|
+
prepared = await self._prepare_kamino_earn_transaction(
|
|
3845
|
+
transaction_base64=str(build["transaction"]),
|
|
3846
|
+
action="deposit",
|
|
3847
|
+
kvault=str(preview["kvault"]),
|
|
3848
|
+
amount_ui=str(preview["amount_ui"]),
|
|
3849
|
+
vault_token_mint=(
|
|
3850
|
+
str(vault_state.get("tokenMint")).strip()
|
|
3851
|
+
if str(vault_state.get("tokenMint") or "").strip()
|
|
3852
|
+
else None
|
|
3853
|
+
),
|
|
3854
|
+
)
|
|
3855
|
+
prepared["build_response"] = build
|
|
3856
|
+
return prepared
|
|
3857
|
+
|
|
3858
|
+
async def execute_kamino_earn_deposit(
|
|
3859
|
+
self,
|
|
3860
|
+
kvault: str,
|
|
3861
|
+
amount_ui: str,
|
|
3862
|
+
approved_preview: dict[str, Any] | None = None,
|
|
3863
|
+
) -> dict[str, Any]:
|
|
3864
|
+
prepared = await self.prepare_kamino_earn_deposit(
|
|
3865
|
+
kvault=kvault,
|
|
3866
|
+
amount_ui=amount_ui,
|
|
3867
|
+
approved_preview=approved_preview,
|
|
3868
|
+
)
|
|
3869
|
+
result = await self._execute_prepared_provider_transaction(prepared, source="kamino")
|
|
3870
|
+
result["build_response"] = prepared.get("build_response")
|
|
3871
|
+
return result
|
|
3872
|
+
|
|
3873
|
+
async def preview_kamino_earn_withdraw(
|
|
3874
|
+
self,
|
|
3875
|
+
kvault: str,
|
|
3876
|
+
amount_ui: str,
|
|
3877
|
+
) -> dict[str, Any]:
|
|
3878
|
+
self._require_mainnet_kamino("Kamino Earn")
|
|
3879
|
+
owner = await self.get_address()
|
|
3880
|
+
if not owner:
|
|
3881
|
+
raise WalletBackendError(
|
|
3882
|
+
"No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
|
|
3883
|
+
)
|
|
3884
|
+
kvault = validate_solana_address(kvault)
|
|
3885
|
+
amount_ui = _require_positive_decimal_string(amount_ui, field_name="amount_ui")
|
|
3886
|
+
vault_snapshot = await self.get_kamino_vaults()
|
|
3887
|
+
vault_entry = self._find_kamino_vault_entry(
|
|
3888
|
+
vaults=list(vault_snapshot["vaults"]),
|
|
3889
|
+
kvault=kvault,
|
|
3890
|
+
)
|
|
3891
|
+
if vault_entry is None:
|
|
3892
|
+
raise WalletBackendError("Requested Kamino Earn vault is not available.")
|
|
3893
|
+
positions_snapshot = await self.get_kamino_earn_positions(user=owner)
|
|
3894
|
+
position_entry = self._find_kamino_earn_position_entry(
|
|
3895
|
+
positions=list(positions_snapshot["positions"]),
|
|
3896
|
+
kvault=kvault,
|
|
3897
|
+
)
|
|
3898
|
+
if position_entry is None:
|
|
3899
|
+
raise WalletBackendError("No Kamino Earn position found for the requested vault.")
|
|
3900
|
+
return {
|
|
3901
|
+
"chain": "solana",
|
|
3902
|
+
"network": self.network,
|
|
3903
|
+
"mode": "preview",
|
|
3904
|
+
"asset_type": "kamino-earn-withdraw",
|
|
3905
|
+
"owner": owner,
|
|
3906
|
+
"kvault": kvault,
|
|
3907
|
+
"amount_ui": amount_ui,
|
|
3908
|
+
"vault_info": vault_entry,
|
|
3909
|
+
"position_info": position_entry,
|
|
3910
|
+
"sign_only": self.sign_only,
|
|
3911
|
+
"can_send": self.get_capabilities().can_send_transaction,
|
|
3912
|
+
"source": "kamino",
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
async def prepare_kamino_earn_withdraw(
|
|
3916
|
+
self,
|
|
3917
|
+
kvault: str,
|
|
3918
|
+
amount_ui: str,
|
|
3919
|
+
approved_preview: dict[str, Any] | None = None,
|
|
3920
|
+
) -> dict[str, Any]:
|
|
3921
|
+
preview = self._kamino_preview_from_approved(
|
|
3922
|
+
approved_preview,
|
|
3923
|
+
asset_type="kamino-earn-withdraw",
|
|
3924
|
+
) or await self.preview_kamino_earn_withdraw(
|
|
3925
|
+
kvault=kvault,
|
|
3926
|
+
amount_ui=amount_ui,
|
|
3927
|
+
)
|
|
3928
|
+
owner = str(preview["owner"])
|
|
3929
|
+
build = await kamino.build_earn_withdraw_transaction(
|
|
3930
|
+
wallet=owner,
|
|
3931
|
+
kvault=str(preview["kvault"]),
|
|
3932
|
+
amount_ui=str(preview["amount_ui"]),
|
|
3933
|
+
)
|
|
3934
|
+
vault_info = preview.get("vault_info") if isinstance(preview.get("vault_info"), dict) else {}
|
|
3935
|
+
vault_state = vault_info.get("state") if isinstance(vault_info.get("state"), dict) else {}
|
|
3936
|
+
prepared = await self._prepare_kamino_earn_transaction(
|
|
3937
|
+
transaction_base64=str(build["transaction"]),
|
|
3938
|
+
action="withdraw",
|
|
3939
|
+
kvault=str(preview["kvault"]),
|
|
3940
|
+
amount_ui=str(preview["amount_ui"]),
|
|
3941
|
+
vault_token_mint=(
|
|
3942
|
+
str(vault_state.get("tokenMint")).strip()
|
|
3943
|
+
if str(vault_state.get("tokenMint") or "").strip()
|
|
3944
|
+
else None
|
|
3945
|
+
),
|
|
3946
|
+
)
|
|
3947
|
+
prepared["build_response"] = build
|
|
3948
|
+
return prepared
|
|
3949
|
+
|
|
3950
|
+
async def execute_kamino_earn_withdraw(
|
|
3951
|
+
self,
|
|
3952
|
+
kvault: str,
|
|
3953
|
+
amount_ui: str,
|
|
3954
|
+
approved_preview: dict[str, Any] | None = None,
|
|
3955
|
+
) -> dict[str, Any]:
|
|
3956
|
+
prepared = await self.prepare_kamino_earn_withdraw(
|
|
3957
|
+
kvault=kvault,
|
|
3958
|
+
amount_ui=amount_ui,
|
|
3959
|
+
approved_preview=approved_preview,
|
|
3960
|
+
)
|
|
3961
|
+
result = await self._execute_prepared_provider_transaction(prepared, source="kamino")
|
|
3962
|
+
result["build_response"] = prepared.get("build_response")
|
|
3963
|
+
return result
|
|
3964
|
+
|
|
3402
3965
|
async def preview_kamino_lend_deposit(
|
|
3403
3966
|
self,
|
|
3404
3967
|
market: str,
|
|
@@ -1026,6 +1026,8 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1026
1026
|
limit: int | None = None,
|
|
1027
1027
|
listed_only: bool = True,
|
|
1028
1028
|
asset_address: str | None = None,
|
|
1029
|
+
min_tvl_usd: float | int | None = None,
|
|
1030
|
+
min_net_apy: float | int | None = None,
|
|
1029
1031
|
order_by: str | None = None,
|
|
1030
1032
|
order_direction: str | None = None,
|
|
1031
1033
|
) -> dict[str, Any]:
|
|
@@ -1039,6 +1041,10 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1039
1041
|
payload["limit"] = int(limit)
|
|
1040
1042
|
if isinstance(asset_address, str) and asset_address.strip():
|
|
1041
1043
|
payload["assetAddress"] = asset_address.strip()
|
|
1044
|
+
if min_tvl_usd is not None:
|
|
1045
|
+
payload["minTotalAssetsUsd"] = min_tvl_usd
|
|
1046
|
+
if min_net_apy is not None:
|
|
1047
|
+
payload["minNetApy"] = min_net_apy
|
|
1042
1048
|
if isinstance(order_by, str) and order_by.strip():
|
|
1043
1049
|
payload["orderBy"] = order_by.strip()
|
|
1044
1050
|
if isinstance(order_direction, str) and order_direction.strip():
|
|
@@ -1054,6 +1060,8 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1054
1060
|
"order_by": data.get("orderBy"),
|
|
1055
1061
|
"order_direction": data.get("orderDirection"),
|
|
1056
1062
|
"asset_address_filter": data.get("assetAddressFilter"),
|
|
1063
|
+
"min_tvl_usd": data.get("minTotalAssetsUsd"),
|
|
1064
|
+
"min_net_apy": data.get("minNetApy"),
|
|
1057
1065
|
"found": bool(data.get("found")) if "found" in data else None,
|
|
1058
1066
|
"vault_count": int(data.get("vaultCount") or 0),
|
|
1059
1067
|
"vault": dict(data.get("vault") or {}) if isinstance(data.get("vault"), dict) else None,
|
|
@@ -1070,6 +1078,8 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1070
1078
|
search: str | None = None,
|
|
1071
1079
|
collateral_asset_address: str | None = None,
|
|
1072
1080
|
loan_asset_address: str | None = None,
|
|
1081
|
+
min_supply_usd: float | int | None = None,
|
|
1082
|
+
min_net_supply_apy: float | int | None = None,
|
|
1073
1083
|
order_by: str | None = None,
|
|
1074
1084
|
order_direction: str | None = None,
|
|
1075
1085
|
) -> dict[str, Any]:
|
|
@@ -1087,6 +1097,10 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1087
1097
|
payload["collateralAssetAddress"] = collateral_asset_address.strip()
|
|
1088
1098
|
if isinstance(loan_asset_address, str) and loan_asset_address.strip():
|
|
1089
1099
|
payload["loanAssetAddress"] = loan_asset_address.strip()
|
|
1100
|
+
if min_supply_usd is not None:
|
|
1101
|
+
payload["minSupplyAssetsUsd"] = min_supply_usd
|
|
1102
|
+
if min_net_supply_apy is not None:
|
|
1103
|
+
payload["minNetSupplyApy"] = min_net_supply_apy
|
|
1090
1104
|
if isinstance(order_by, str) and order_by.strip():
|
|
1091
1105
|
payload["orderBy"] = order_by.strip()
|
|
1092
1106
|
if isinstance(order_direction, str) and order_direction.strip():
|
|
@@ -1104,6 +1118,8 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
1104
1118
|
"search": data.get("search"),
|
|
1105
1119
|
"collateral_asset_filter": data.get("collateralAssetFilter"),
|
|
1106
1120
|
"loan_asset_filter": data.get("loanAssetFilter"),
|
|
1121
|
+
"min_supply_usd": data.get("minSupplyAssetsUsd"),
|
|
1122
|
+
"min_net_supply_apy": data.get("minNetSupplyApy"),
|
|
1107
1123
|
"found": bool(data.get("found")) if "found" in data else None,
|
|
1108
1124
|
"market_count": int(data.get("marketCount") or 0),
|
|
1109
1125
|
"market": dict(data.get("market") or {}) if isinstance(data.get("market"), dict) else None,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agent-wallet",
|
|
3
3
|
"name": "Agent Wallet",
|
|
4
4
|
"description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.67",
|
|
6
6
|
"skills": ["skills/wallet-operator"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
},
|
|
120
120
|
"kaminoBaseUrl": {
|
|
121
121
|
"type": "string",
|
|
122
|
-
"description": "Optional Kamino REST API base URL for lending reads
|
|
122
|
+
"description": "Optional Kamino REST API base URL for lending and Earn reads plus transaction building."
|
|
123
123
|
},
|
|
124
124
|
"kaminoProgramId": {
|
|
125
125
|
"type": "string",
|