@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.
Files changed (28) hide show
  1. package/.openclaw/extensions/agent-wallet/index.ts +82 -0
  2. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +7 -1
  3. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  4. package/VERSION +1 -1
  5. package/agent-wallet/README.md +6 -0
  6. package/agent-wallet/agent_wallet/__init__.py +1 -1
  7. package/agent-wallet/agent_wallet/openclaw_adapter.py +485 -4
  8. package/agent-wallet/agent_wallet/providers/kamino.py +169 -3
  9. package/agent-wallet/agent_wallet/providers/x402.py +229 -148
  10. package/agent-wallet/agent_wallet/transaction_policy.py +60 -0
  11. package/agent-wallet/agent_wallet/wallet_layer/base.py +124 -0
  12. package/agent-wallet/agent_wallet/wallet_layer/solana.py +563 -0
  13. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +16 -0
  14. package/agent-wallet/openclaw.plugin.json +2 -2
  15. package/agent-wallet/pyproject.toml +1 -1
  16. package/agent-wallet/scripts/install_openclaw_local_config.py +6 -0
  17. package/agent-wallet/skills/wallet-operator/SKILL.md +6 -4
  18. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  19. package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
  20. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  21. package/codex/plugins/agent-wallet/server.py +52 -3
  22. package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +1 -1
  23. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  24. package/install-from-github.sh +6 -0
  25. package/package.json +1 -1
  26. package/wdk-btc-wallet/package.json +1 -1
  27. package/wdk-evm-wallet/package.json +1 -1
  28. 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.65",
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 and transaction building."
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",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.65"
7
+ version = "0.1.67"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [