@agentlayer.tech/wallet 0.1.66 → 0.1.68

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.
@@ -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,
@@ -568,6 +568,24 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
568
568
  except ValueError as exc:
569
569
  raise WalletBackendError("wdk-evm-wallet returned an invalid x402 EVM signature.") from exc
570
570
 
571
+ async def sign_message(self, message: bytes | str) -> str:
572
+ text = message.decode("utf-8") if isinstance(message, bytes) else str(message)
573
+ if not text:
574
+ raise WalletBackendError("message must be a non-empty string.")
575
+ data = await self.client.post(
576
+ "/v1/evm/message/sign",
577
+ {
578
+ "walletId": self.wallet_id,
579
+ "accountIndex": self.account_index,
580
+ "network": self.network,
581
+ "message": text,
582
+ },
583
+ )
584
+ signature = str(data.get("signature") or "").strip()
585
+ if not signature:
586
+ raise WalletBackendError("wdk-evm-wallet did not return a message signature.")
587
+ return signature
588
+
571
589
  async def get_balance(self, address: str | None = None) -> dict[str, Any]:
572
590
  resolved_address = await self.get_address()
573
591
  if address is not None and address.strip() and address.strip() != resolved_address:
@@ -1026,6 +1044,8 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1026
1044
  limit: int | None = None,
1027
1045
  listed_only: bool = True,
1028
1046
  asset_address: str | None = None,
1047
+ min_tvl_usd: float | int | None = None,
1048
+ min_net_apy: float | int | None = None,
1029
1049
  order_by: str | None = None,
1030
1050
  order_direction: str | None = None,
1031
1051
  ) -> dict[str, Any]:
@@ -1039,6 +1059,10 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1039
1059
  payload["limit"] = int(limit)
1040
1060
  if isinstance(asset_address, str) and asset_address.strip():
1041
1061
  payload["assetAddress"] = asset_address.strip()
1062
+ if min_tvl_usd is not None:
1063
+ payload["minTotalAssetsUsd"] = min_tvl_usd
1064
+ if min_net_apy is not None:
1065
+ payload["minNetApy"] = min_net_apy
1042
1066
  if isinstance(order_by, str) and order_by.strip():
1043
1067
  payload["orderBy"] = order_by.strip()
1044
1068
  if isinstance(order_direction, str) and order_direction.strip():
@@ -1054,6 +1078,8 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1054
1078
  "order_by": data.get("orderBy"),
1055
1079
  "order_direction": data.get("orderDirection"),
1056
1080
  "asset_address_filter": data.get("assetAddressFilter"),
1081
+ "min_tvl_usd": data.get("minTotalAssetsUsd"),
1082
+ "min_net_apy": data.get("minNetApy"),
1057
1083
  "found": bool(data.get("found")) if "found" in data else None,
1058
1084
  "vault_count": int(data.get("vaultCount") or 0),
1059
1085
  "vault": dict(data.get("vault") or {}) if isinstance(data.get("vault"), dict) else None,
@@ -1070,6 +1096,8 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1070
1096
  search: str | None = None,
1071
1097
  collateral_asset_address: str | None = None,
1072
1098
  loan_asset_address: str | None = None,
1099
+ min_supply_usd: float | int | None = None,
1100
+ min_net_supply_apy: float | int | None = None,
1073
1101
  order_by: str | None = None,
1074
1102
  order_direction: str | None = None,
1075
1103
  ) -> dict[str, Any]:
@@ -1087,6 +1115,10 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1087
1115
  payload["collateralAssetAddress"] = collateral_asset_address.strip()
1088
1116
  if isinstance(loan_asset_address, str) and loan_asset_address.strip():
1089
1117
  payload["loanAssetAddress"] = loan_asset_address.strip()
1118
+ if min_supply_usd is not None:
1119
+ payload["minSupplyAssetsUsd"] = min_supply_usd
1120
+ if min_net_supply_apy is not None:
1121
+ payload["minNetSupplyApy"] = min_net_supply_apy
1090
1122
  if isinstance(order_by, str) and order_by.strip():
1091
1123
  payload["orderBy"] = order_by.strip()
1092
1124
  if isinstance(order_direction, str) and order_direction.strip():
@@ -1104,6 +1136,8 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1104
1136
  "search": data.get("search"),
1105
1137
  "collateral_asset_filter": data.get("collateralAssetFilter"),
1106
1138
  "loan_asset_filter": data.get("loanAssetFilter"),
1139
+ "min_supply_usd": data.get("minSupplyAssetsUsd"),
1140
+ "min_net_supply_apy": data.get("minNetSupplyApy"),
1107
1141
  "found": bool(data.get("found")) if "found" in data else None,
1108
1142
  "market_count": int(data.get("marketCount") or 0),
1109
1143
  "market": dict(data.get("market") or {}) if isinstance(data.get("market"), dict) else None,