@agentlayer.tech/wallet 0.1.66 → 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.
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
6
+ import time
5
7
  from typing import Any
6
8
 
7
9
  from agent_wallet.config import normalize_solana_network, settings
@@ -9,6 +11,41 @@ from agent_wallet.exceptions import ProviderError
9
11
  from agent_wallet.http_client import get_client
10
12
 
11
13
  KAMINO_BUILD_TIMEOUT_SECONDS = 20.0
14
+ # Discovery data only (market/vault catalogs and rate metrics) — never
15
+ # user-specific data such as obligations, positions, rewards, or portfolios.
16
+ # Catalogs change when Kamino ships a market/vault (days); rate metrics drift
17
+ # slower than the API's own aggregation window over these TTLs. The cache is
18
+ # long-lived only inside the resident read worker; cold CLI runs start empty.
19
+ MARKETS_CACHE_TTL_SECONDS = 600.0
20
+ RESERVES_CACHE_TTL_SECONDS = 180.0
21
+ EARN_VAULTS_CACHE_TTL_SECONDS = 600.0
22
+ EARN_VAULT_METRICS_CACHE_TTL_SECONDS = 180.0
23
+ _DISCOVERY_CACHE_MAX_ENTRIES = 128
24
+ _discovery_cache: dict[str, tuple[float, str]] = {}
25
+
26
+
27
+ def _discovery_cache_get(cache_key: str) -> Any | None:
28
+ entry = _discovery_cache.get(cache_key)
29
+ if entry is None:
30
+ return None
31
+ expires_at, payload_text = entry
32
+ if expires_at <= time.monotonic():
33
+ _discovery_cache.pop(cache_key, None)
34
+ return None
35
+ return json.loads(payload_text)
36
+
37
+
38
+ def _discovery_cache_set(cache_key: str, payload: Any, ttl_seconds: float) -> None:
39
+ try:
40
+ payload_text = json.dumps(payload)
41
+ except (TypeError, ValueError):
42
+ return
43
+ if len(_discovery_cache) >= _DISCOVERY_CACHE_MAX_ENTRIES:
44
+ for stale_key in sorted(_discovery_cache, key=lambda key: _discovery_cache[key][0])[
45
+ : len(_discovery_cache) - _DISCOVERY_CACHE_MAX_ENTRIES + 1
46
+ ]:
47
+ _discovery_cache.pop(stale_key, None)
48
+ _discovery_cache[cache_key] = (time.monotonic() + ttl_seconds, payload_text)
12
49
 
13
50
 
14
51
  def _normalized_api_base() -> str:
@@ -48,6 +85,10 @@ def _env_name(network: str) -> str:
48
85
 
49
86
  async def fetch_lend_markets() -> dict[str, Any]:
50
87
  """Fetch Kamino lending markets for the configured program id."""
88
+ cache_key = f"markets:{settings.kamino_program_id}"
89
+ cached = _discovery_cache_get(cache_key)
90
+ if cached is not None:
91
+ return cached
51
92
  client = get_client()
52
93
  response = await client.get(
53
94
  f"{_normalized_api_base()}/v2/kamino-market",
@@ -55,11 +96,85 @@ async def fetch_lend_markets() -> dict[str, Any]:
55
96
  )
56
97
  if response.status_code != 200:
57
98
  raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
58
- return _normalize_named_list_response(
99
+ result = _normalize_named_list_response(
59
100
  response.json(),
60
101
  key="markets",
61
102
  provider_name="kamino",
62
103
  )
104
+ _discovery_cache_set(cache_key, result, MARKETS_CACHE_TTL_SECONDS)
105
+ return result
106
+
107
+
108
+ async def fetch_portfolio(*, user: str) -> dict[str, Any]:
109
+ """Fetch the unified Kamino product portfolio for one wallet."""
110
+ client = get_client()
111
+ response = await client.get(f"{_normalized_api_base()}/portfolio/{user}")
112
+ if response.status_code != 200:
113
+ raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
114
+ data = response.json()
115
+ if not isinstance(data, dict):
116
+ raise ProviderError("kamino", "Unexpected portfolio response from Kamino.")
117
+ return data
118
+
119
+
120
+ async def fetch_earn_vaults() -> dict[str, Any]:
121
+ """Fetch the list of Kamino Earn vaults."""
122
+ cache_key = "earn_vaults"
123
+ cached = _discovery_cache_get(cache_key)
124
+ if cached is not None:
125
+ return cached
126
+ client = get_client()
127
+ response = await client.get(f"{_normalized_api_base()}/kvaults/vaults")
128
+ if response.status_code != 200:
129
+ raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
130
+ result = _normalize_named_list_response(
131
+ response.json(),
132
+ key="vaults",
133
+ provider_name="kamino",
134
+ )
135
+ _discovery_cache_set(cache_key, result, EARN_VAULTS_CACHE_TTL_SECONDS)
136
+ return result
137
+
138
+
139
+ async def fetch_earn_vault_metrics(*, vault: str, network: str) -> dict[str, Any]:
140
+ """Fetch APY/TVL metrics for one Kamino Earn vault."""
141
+ env = _env_name(network)
142
+ cache_key = f"earn_vault_metrics:{vault}:{env}"
143
+ cached = _discovery_cache_get(cache_key)
144
+ if cached is not None:
145
+ return cached
146
+ client = get_client()
147
+ response = await client.get(
148
+ f"{_normalized_api_base()}/kvaults/{vault}/metrics",
149
+ params={"env": env},
150
+ )
151
+ if response.status_code != 200:
152
+ raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
153
+ data = response.json()
154
+ if not isinstance(data, dict):
155
+ raise ProviderError("kamino", "Unexpected vault metrics response from Kamino.")
156
+ _discovery_cache_set(cache_key, data, EARN_VAULT_METRICS_CACHE_TTL_SECONDS)
157
+ return data
158
+
159
+
160
+ async def fetch_earn_user_positions(
161
+ *,
162
+ user: str,
163
+ network: str,
164
+ ) -> dict[str, Any]:
165
+ """Fetch all Kamino Earn vault positions for a wallet."""
166
+ client = get_client()
167
+ response = await client.get(
168
+ f"{_normalized_api_base()}/kvaults/users/{user}/positions",
169
+ params={"env": _env_name(network)},
170
+ )
171
+ if response.status_code != 200:
172
+ raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
173
+ return _normalize_named_list_response(
174
+ response.json(),
175
+ key="positions",
176
+ provider_name="kamino",
177
+ )
63
178
 
64
179
 
65
180
  async def fetch_lend_market_reserves(
@@ -68,18 +183,25 @@ async def fetch_lend_market_reserves(
68
183
  network: str,
69
184
  ) -> dict[str, Any]:
70
185
  """Fetch reserve metrics for one Kamino lending market."""
186
+ env = _env_name(network)
187
+ cache_key = f"reserves:{market}:{env}"
188
+ cached = _discovery_cache_get(cache_key)
189
+ if cached is not None:
190
+ return cached
71
191
  client = get_client()
72
192
  response = await client.get(
73
193
  f"{_normalized_api_base()}/kamino-market/{market}/reserves/metrics",
74
- params={"env": _env_name(network)},
194
+ params={"env": env},
75
195
  )
76
196
  if response.status_code != 200:
77
197
  raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
78
- return _normalize_named_list_response(
198
+ result = _normalize_named_list_response(
79
199
  response.json(),
80
200
  key="reserves",
81
201
  provider_name="kamino",
82
202
  )
203
+ _discovery_cache_set(cache_key, result, RESERVES_CACHE_TTL_SECONDS)
204
+ return result
83
205
 
84
206
 
85
207
  async def fetch_lend_user_obligations(
@@ -236,3 +358,47 @@ async def build_lend_repay_transaction(
236
358
  if response.status_code != 200:
237
359
  raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
238
360
  return _normalized_tx_response(response.json(), provider_name="kamino")
361
+
362
+
363
+ async def build_earn_deposit_transaction(
364
+ *,
365
+ wallet: str,
366
+ kvault: str,
367
+ amount_ui: str,
368
+ ) -> dict[str, Any]:
369
+ """Build an unsigned Kamino Earn deposit transaction."""
370
+ client = get_client()
371
+ response = await client.post(
372
+ f"{_normalized_api_base()}/ktx/kvault/deposit",
373
+ json={
374
+ "wallet": wallet,
375
+ "kvault": kvault,
376
+ "amount": amount_ui,
377
+ },
378
+ timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
379
+ )
380
+ if response.status_code != 200:
381
+ raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
382
+ return _normalized_tx_response(response.json(), provider_name="kamino")
383
+
384
+
385
+ async def build_earn_withdraw_transaction(
386
+ *,
387
+ wallet: str,
388
+ kvault: str,
389
+ amount_ui: str,
390
+ ) -> dict[str, Any]:
391
+ """Build an unsigned Kamino Earn withdraw transaction."""
392
+ client = get_client()
393
+ response = await client.post(
394
+ f"{_normalized_api_base()}/ktx/kvault/withdraw",
395
+ json={
396
+ "wallet": wallet,
397
+ "kvault": kvault,
398
+ "amount": amount_ui,
399
+ },
400
+ timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
401
+ )
402
+ if response.status_code != 200:
403
+ raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
404
+ return _normalized_tx_response(response.json(), provider_name="kamino")
@@ -18,6 +18,7 @@ JUPITER_V6_PROGRAM_ID = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5Nt7NQYjN"
18
18
  JUPITER_ULTRA_EXACT_OUT_PROGRAM_ID = "j1o2qRpjcyUwEvwtcfhEQefh773ZgjxcVRry7LDqg5X"
19
19
  JUPITER_DCA_PROGRAM_ID = "DCA265Vj8a7wYymQG8LqM3m7A4QeV9hiC7VYh4S6Jsa"
20
20
  KAMINO_LEND_PROGRAM_ID = "KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD"
21
+ KAMINO_EARN_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd"
21
22
  NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112"
22
23
  DEFAULT_NATIVE_SOL_EXTRA_SPEND_ALLOWANCE_LAMPORTS = 10_000_000
23
24
 
@@ -45,6 +46,7 @@ FORBIDDEN_PROGRAMS = {
45
46
  }
46
47
  KAMINO_ALLOWED_PROGRAMS = CORE_PROGRAM_IDS | {
47
48
  KAMINO_LEND_PROGRAM_ID,
49
+ KAMINO_EARN_PROGRAM_ID,
48
50
  }
49
51
 
50
52
 
@@ -513,6 +515,64 @@ def verify_provider_kamino_lend_transaction(
513
515
  }
514
516
 
515
517
 
518
+ def verify_provider_kamino_earn_transaction(
519
+ message: Any,
520
+ *,
521
+ wallet_address: str,
522
+ vault_address: str,
523
+ action: str,
524
+ vault_token_mint: str | None = None,
525
+ loaded_addresses: list[str] | None = None,
526
+ ) -> dict[str, Any]:
527
+ binding = _assert_basic_wallet_binding(
528
+ message,
529
+ wallet_address=wallet_address,
530
+ loaded_addresses=loaded_addresses,
531
+ )
532
+ keys = binding["account_keys"]
533
+ if vault_address not in keys:
534
+ raise WalletBackendError(
535
+ f"{action} transaction does not reference the expected Kamino Earn vault."
536
+ )
537
+ if vault_token_mint and vault_token_mint not in keys:
538
+ raise WalletBackendError(
539
+ f"{action} transaction does not reference the expected Kamino Earn vault token mint."
540
+ )
541
+ program_ids = _program_ids(message, loaded_addresses)
542
+ unknown_program_ids = _assert_program_allowlist(
543
+ program_ids,
544
+ allowed_programs=KAMINO_ALLOWED_PROGRAMS,
545
+ label=action,
546
+ reject_unknown=False,
547
+ )
548
+ recognized_kamino_program_ids = [
549
+ pid for pid in program_ids if pid == KAMINO_EARN_PROGRAM_ID
550
+ ]
551
+ if not recognized_kamino_program_ids:
552
+ raise WalletBackendError(
553
+ f"{action} transaction does not include the expected Kamino Earn program."
554
+ )
555
+ return {
556
+ "wallet_address": wallet_address,
557
+ "fee_payer": binding["fee_payer"],
558
+ "required_signer_keys": binding["required_signer_keys"],
559
+ "required_signature_count": binding["required_signature_count"],
560
+ "wallet_signer_index": binding["wallet_signer_index"],
561
+ "sponsored_fee_payer": binding["sponsored_fee_payer"],
562
+ "program_ids": program_ids,
563
+ "unknown_program_ids": unknown_program_ids,
564
+ "recognized_kamino_program_ids": recognized_kamino_program_ids,
565
+ "has_recognized_kamino_program": True,
566
+ "non_core_program_ids": [pid for pid in program_ids if pid not in CORE_PROGRAM_IDS],
567
+ "account_key_count": len(keys),
568
+ "instruction_count": len(_compiled_instructions(message)),
569
+ "vault_address": vault_address,
570
+ "vault_token_mint": vault_token_mint,
571
+ "action": action,
572
+ "verified": True,
573
+ }
574
+
575
+
516
576
  def verify_provider_flash_transaction(
517
577
  message: Any,
518
578
  *,
@@ -549,6 +549,24 @@ class AgentWalletBackend(ABC):
549
549
  async def get_kamino_lend_markets(self) -> dict[str, Any]:
550
550
  raise WalletBackendError(f"{self.name} does not support Kamino market lookup.")
551
551
 
552
+ async def get_kamino_portfolio(self, user: str | None = None) -> dict[str, Any]:
553
+ raise WalletBackendError(f"{self.name} does not support Kamino portfolio lookup.")
554
+
555
+ async def get_kamino_vaults(
556
+ self,
557
+ vault_address: str | None = None,
558
+ token_mint: str | None = None,
559
+ include_metrics: bool = False,
560
+ limit: int | None = None,
561
+ ) -> dict[str, Any]:
562
+ raise WalletBackendError(f"{self.name} does not support Kamino vault lookup.")
563
+
564
+ async def get_kamino_earn_positions(self, user: str | None = None) -> dict[str, Any]:
565
+ raise WalletBackendError(f"{self.name} does not support Kamino Earn position lookup.")
566
+
567
+ async def get_kamino_liquidity_positions(self, user: str | None = None) -> dict[str, Any]:
568
+ raise WalletBackendError(f"{self.name} does not support Kamino liquidity position lookup.")
569
+
552
570
  async def get_kamino_lend_market_reserves(self, market: str) -> dict[str, Any]:
553
571
  raise WalletBackendError(f"{self.name} does not support Kamino reserve lookup.")
554
572
 
@@ -791,6 +809,112 @@ class AgentWalletBackend(ABC):
791
809
  ) -> dict[str, Any]:
792
810
  raise WalletBackendError(f"{self.name} does not support Kamino repays.")
793
811
 
812
+ async def preview_kamino_earn_deposit(
813
+ self,
814
+ kvault: str,
815
+ amount_ui: str,
816
+ ) -> dict[str, Any]:
817
+ raise WalletBackendError(f"{self.name} does not support Kamino Earn deposit previews.")
818
+
819
+ def _build_kamino_earn_intent_preview(
820
+ self,
821
+ base_preview: dict[str, Any],
822
+ *,
823
+ valid_for_seconds: int,
824
+ ) -> dict[str, Any]:
825
+ if valid_for_seconds <= 0 or valid_for_seconds > 300:
826
+ raise WalletBackendError("valid_for_seconds must be between 1 and 300.")
827
+ try:
828
+ can_send = bool(self.get_capabilities().can_send_transaction)
829
+ except Exception:
830
+ can_send = bool(base_preview.get("can_send"))
831
+ return {
832
+ "chain": "solana",
833
+ "network": getattr(self, "network", "mainnet"),
834
+ "mode": "intent_preview",
835
+ "asset_type": "kamino-earn-intent",
836
+ "kamino_operation": base_preview["asset_type"],
837
+ "owner": base_preview["owner"],
838
+ "kvault": base_preview["kvault"],
839
+ "amount_ui": base_preview["amount_ui"],
840
+ "vault_info": base_preview.get("vault_info"),
841
+ "position_info": base_preview.get("position_info"),
842
+ "recipient_policy": "owner-only",
843
+ "spend_policy": "exact-amount",
844
+ "valid_for_seconds": valid_for_seconds,
845
+ "valid_until_epoch_seconds": int(time.time()) + valid_for_seconds,
846
+ "intent_note": (
847
+ "This is an intent approval preview. Execute re-derives the Kamino "
848
+ "Earn transaction and only signs/sends if it remains within these approved parameters."
849
+ ),
850
+ "can_send": can_send,
851
+ "sign_only": bool(getattr(self, "sign_only", False)),
852
+ "source": "kamino-intent",
853
+ }
854
+
855
+ async def preview_kamino_earn_deposit_intent(
856
+ self,
857
+ kvault: str,
858
+ amount_ui: str,
859
+ valid_for_seconds: int = 120,
860
+ ) -> dict[str, Any]:
861
+ base = await self.preview_kamino_earn_deposit(
862
+ kvault=kvault,
863
+ amount_ui=amount_ui,
864
+ )
865
+ return self._build_kamino_earn_intent_preview(base, valid_for_seconds=valid_for_seconds)
866
+
867
+ async def prepare_kamino_earn_deposit(
868
+ self,
869
+ kvault: str,
870
+ amount_ui: str,
871
+ approved_preview: dict[str, Any] | None = None,
872
+ ) -> dict[str, Any]:
873
+ raise WalletBackendError(f"{self.name} does not support Kamino Earn deposit preparation.")
874
+
875
+ async def execute_kamino_earn_deposit(
876
+ self,
877
+ kvault: str,
878
+ amount_ui: str,
879
+ approved_preview: dict[str, Any] | None = None,
880
+ ) -> dict[str, Any]:
881
+ raise WalletBackendError(f"{self.name} does not support Kamino Earn deposits.")
882
+
883
+ async def preview_kamino_earn_withdraw(
884
+ self,
885
+ kvault: str,
886
+ amount_ui: str,
887
+ ) -> dict[str, Any]:
888
+ raise WalletBackendError(f"{self.name} does not support Kamino Earn withdraw previews.")
889
+
890
+ async def preview_kamino_earn_withdraw_intent(
891
+ self,
892
+ kvault: str,
893
+ amount_ui: str,
894
+ valid_for_seconds: int = 120,
895
+ ) -> dict[str, Any]:
896
+ base = await self.preview_kamino_earn_withdraw(
897
+ kvault=kvault,
898
+ amount_ui=amount_ui,
899
+ )
900
+ return self._build_kamino_earn_intent_preview(base, valid_for_seconds=valid_for_seconds)
901
+
902
+ async def prepare_kamino_earn_withdraw(
903
+ self,
904
+ kvault: str,
905
+ amount_ui: str,
906
+ approved_preview: dict[str, Any] | None = None,
907
+ ) -> dict[str, Any]:
908
+ raise WalletBackendError(f"{self.name} does not support Kamino Earn withdraw preparation.")
909
+
910
+ async def execute_kamino_earn_withdraw(
911
+ self,
912
+ kvault: str,
913
+ amount_ui: str,
914
+ approved_preview: dict[str, Any] | None = None,
915
+ ) -> dict[str, Any]:
916
+ raise WalletBackendError(f"{self.name} does not support Kamino Earn withdraws.")
917
+
794
918
  async def preview_close_empty_token_accounts(
795
919
  self,
796
920
  limit: int = 8,