@agentlayer.tech/wallet 0.1.28 → 0.1.30

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 (56) hide show
  1. package/.openclaw/extensions/agent-wallet/README.md +4 -5
  2. package/.openclaw/extensions/agent-wallet/dist/index.js +29 -20
  3. package/.openclaw/extensions/agent-wallet/index.ts +29 -20
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +2 -2
  5. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  6. package/CHANGELOG.md +38 -0
  7. package/README.md +9 -0
  8. package/agent-wallet/README.md +18 -22
  9. package/agent-wallet/agent_wallet/bootstrap.py +28 -12
  10. package/agent-wallet/agent_wallet/btc_user_wallets.py +2 -7
  11. package/agent-wallet/agent_wallet/config.py +99 -22
  12. package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -14
  13. package/agent-wallet/agent_wallet/openclaw_adapter.py +28 -32
  14. package/agent-wallet/agent_wallet/openclaw_runtime.py +3 -12
  15. package/agent-wallet/agent_wallet/providers/kamino.py +21 -4
  16. package/agent-wallet/agent_wallet/providers/solana_rpc.py +0 -23
  17. package/agent-wallet/agent_wallet/providers/x402.py +4 -9
  18. package/agent-wallet/agent_wallet/user_wallets.py +4 -3
  19. package/agent-wallet/agent_wallet/wallet_layer/base.py +3 -3
  20. package/agent-wallet/agent_wallet/wallet_layer/factory.py +8 -5
  21. package/agent-wallet/agent_wallet/wallet_layer/solana.py +437 -44
  22. package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +2 -8
  23. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +2 -12
  24. package/agent-wallet/examples/openclaw_runtime_onboarding.py +1 -1
  25. package/agent-wallet/examples/openclaw_user_wallet_example.py +1 -1
  26. package/agent-wallet/openclaw.plugin.json +1 -1
  27. package/agent-wallet/pyproject.toml +2 -1
  28. package/agent-wallet/scripts/bootstrap_openclaw_btc.py +3 -5
  29. package/agent-wallet/scripts/bootstrap_openclaw_evm.py +2 -12
  30. package/agent-wallet/scripts/build_release_bundle.py +1 -0
  31. package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1 -4
  32. package/agent-wallet/scripts/install_agent_wallet.py +1 -0
  33. package/agent-wallet/scripts/install_openclaw_local_config.py +4 -6
  34. package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +2 -4
  35. package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +2 -15
  36. package/agent-wallet/scripts/reveal_btc_seed.sh +7 -16
  37. package/agent-wallet/scripts/setup_btc_wallet.sh +7 -16
  38. package/agent-wallet/scripts/setup_evm_wallet.sh +1 -11
  39. package/agent-wallet/scripts/switch_openclaw_wallet_network.py +4 -1
  40. package/agent-wallet/skills/wallet-operator/SKILL.md +0 -1
  41. package/bin/openclaw-agent-wallet.mjs +289 -0
  42. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +20 -0
  43. package/claude-code/plugins/agent-wallet/.mcp.json +14 -0
  44. package/claude-code/plugins/agent-wallet/README.md +65 -0
  45. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +34 -0
  46. package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
  47. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +38 -0
  48. package/codex/plugins/agent-wallet/.mcp.json +15 -0
  49. package/codex/plugins/agent-wallet/README.md +39 -0
  50. package/codex/plugins/agent-wallet/scripts/run_mcp.sh +21 -0
  51. package/codex/plugins/agent-wallet/server.py +1077 -0
  52. package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
  53. package/hermes/plugins/agent_wallet/schemas.py +2 -2
  54. package/hermes/plugins/agent_wallet/tools.py +17 -3
  55. package/package.json +6 -1
  56. package/setup.sh +2 -0
@@ -82,6 +82,79 @@ class Settings(BaseSettings):
82
82
  settings = Settings()
83
83
 
84
84
 
85
+ def normalize_solana_network(network: str | None) -> str:
86
+ """Canonicalize supported Solana network names and reject test clusters."""
87
+ normalized = str(network or "").strip().lower() or "mainnet"
88
+ aliases = {
89
+ "mainnet-beta": "mainnet",
90
+ }
91
+ normalized = aliases.get(normalized, normalized)
92
+ if normalized in {"devnet", "testnet"}:
93
+ from agent_wallet.wallet_layer.base import WalletBackendError
94
+
95
+ raise WalletBackendError(
96
+ "Solana devnet/testnet are no longer supported by agent-wallet. "
97
+ "Use mainnet or remove the Solana network override."
98
+ )
99
+ if normalized != "mainnet":
100
+ from agent_wallet.wallet_layer.base import WalletBackendError
101
+
102
+ raise WalletBackendError(
103
+ f"Unsupported Solana network: {normalized}. Only mainnet is supported."
104
+ )
105
+ return "mainnet"
106
+
107
+
108
+ def normalize_evm_network(network: str | None) -> str:
109
+ """Canonicalize supported EVM network names and reject testnets."""
110
+ normalized = str(network or "").strip().lower() or "ethereum"
111
+ aliases = {
112
+ "mainnet": "ethereum",
113
+ "eth": "ethereum",
114
+ "eth-mainnet": "ethereum",
115
+ "base-mainnet": "base",
116
+ }
117
+ normalized = aliases.get(normalized, normalized)
118
+ if normalized in {"sepolia", "base-sepolia", "base_sepolia"}:
119
+ from agent_wallet.wallet_layer.base import WalletBackendError
120
+
121
+ raise WalletBackendError(
122
+ "EVM testnets are no longer supported by agent-wallet. Use ethereum or base."
123
+ )
124
+ if normalized not in {"ethereum", "base"}:
125
+ from agent_wallet.wallet_layer.base import WalletBackendError
126
+
127
+ raise WalletBackendError(
128
+ f"Unsupported EVM network: {normalized}. Use ethereum or base."
129
+ )
130
+ return normalized
131
+
132
+
133
+ def normalize_btc_network(network: str | None) -> str:
134
+ """Canonicalize supported BTC network names and reject non-mainnet chains."""
135
+ normalized = str(network or "").strip().lower() or "bitcoin"
136
+ aliases = {
137
+ "mainnet": "bitcoin",
138
+ "btc": "bitcoin",
139
+ "bitcoin-mainnet": "bitcoin",
140
+ "bitcoin_mainnet": "bitcoin",
141
+ }
142
+ normalized = aliases.get(normalized, normalized)
143
+ if normalized in {"testnet", "regtest"}:
144
+ from agent_wallet.wallet_layer.base import WalletBackendError
145
+
146
+ raise WalletBackendError(
147
+ "Bitcoin testnet/regtest are no longer supported by agent-wallet. Use bitcoin."
148
+ )
149
+ if normalized != "bitcoin":
150
+ from agent_wallet.wallet_layer.base import WalletBackendError
151
+
152
+ raise WalletBackendError(
153
+ f"Unsupported Bitcoin network: {normalized}. Only bitcoin is supported."
154
+ )
155
+ return "bitcoin"
156
+
157
+
85
158
  def _normalize_provider_mode(value: str | None) -> str:
86
159
  mode = (value or "").strip().lower()
87
160
  if not mode:
@@ -129,20 +202,20 @@ def resolve_openclaw_home() -> Path:
129
202
 
130
203
  def default_solana_wallet_path(network: str) -> Path:
131
204
  """Return the default keypair path for a Solana wallet."""
132
- return resolve_openclaw_home() / "wallets" / f"solana-{network}-agent.json"
205
+ normalized_network = normalize_solana_network(network)
206
+ return resolve_openclaw_home() / "wallets" / f"solana-{normalized_network}-agent.json"
133
207
 
134
208
 
135
209
  def resolve_solana_rpc_url(network: str, configured: str) -> str:
136
210
  """Resolve the effective Solana RPC URL from network + optional override."""
211
+ normalized_network = normalize_solana_network(network)
137
212
  if configured.strip():
138
213
  return configured.strip()
139
214
 
140
215
  mapping = {
141
216
  "mainnet": "https://api.mainnet-beta.solana.com",
142
- "devnet": "https://api.devnet.solana.com",
143
- "testnet": "https://api.testnet.solana.com",
144
217
  }
145
- return mapping.get(network.strip().lower(), mapping["mainnet"])
218
+ return mapping.get(normalized_network, mapping["mainnet"])
146
219
 
147
220
 
148
221
  def resolve_solana_rpc_urls(
@@ -151,17 +224,18 @@ def resolve_solana_rpc_urls(
151
224
  configured_list: str = "",
152
225
  ) -> list[str]:
153
226
  """Resolve the ordered list of Solana RPC URLs to try."""
227
+ normalized_network = normalize_solana_network(network)
154
228
  candidates: list[str] = []
155
229
  for raw in (configured_list or "").split(","):
156
230
  value = raw.strip()
157
231
  if value and value not in candidates:
158
232
  candidates.append(value)
159
233
 
160
- primary = resolve_solana_rpc_url(network, configured)
234
+ primary = resolve_solana_rpc_url(normalized_network, configured)
161
235
  if primary and primary not in candidates:
162
236
  candidates.insert(0, primary)
163
237
 
164
- official = resolve_solana_rpc_url(network, "")
238
+ official = resolve_solana_rpc_url(normalized_network, "")
165
239
  if official and official not in candidates:
166
240
  candidates.append(official)
167
241
 
@@ -169,7 +243,8 @@ def resolve_solana_rpc_urls(
169
243
 
170
244
 
171
245
  def _build_provider_gateway_rpc_url(base_url: str, provider: str, network: str) -> str:
172
- return f"gateway::{provider}::{network.strip().lower()}::{base_url.rstrip('/')}/v1/rpc"
246
+ normalized_network = normalize_solana_network(network)
247
+ return f"gateway::{provider}::{normalized_network}::{base_url.rstrip('/')}/v1/rpc"
173
248
 
174
249
 
175
250
  def resolve_runtime_solana_rpc_config(
@@ -185,6 +260,7 @@ def resolve_runtime_solana_rpc_config(
185
260
  3. shared proxy gateway
186
261
  4. public official fallback
187
262
  """
263
+ normalized_network = normalize_solana_network(network)
188
264
  mode = _normalize_provider_mode(
189
265
  os.getenv("SOLANA_RPC_PROVIDER_MODE", settings.solana_rpc_provider_mode)
190
266
  )
@@ -200,10 +276,10 @@ def resolve_runtime_solana_rpc_config(
200
276
  "mode": "user_direct",
201
277
  "provider": "custom",
202
278
  "transport": "direct",
203
- "rpc_urls": resolve_solana_rpc_urls(network, env_primary, env_list),
279
+ "rpc_urls": resolve_solana_rpc_urls(normalized_network, env_primary, env_list),
204
280
  }
205
281
  if env_list:
206
- official = resolve_solana_rpc_url(network, "")
282
+ official = resolve_solana_rpc_url(normalized_network, "")
207
283
  candidates = [item.strip() for item in env_list.split(",") if item.strip()]
208
284
  if official and official not in candidates:
209
285
  candidates.append(official)
@@ -218,16 +294,15 @@ def resolve_runtime_solana_rpc_config(
218
294
  if alchemy_key:
219
295
  alchemy_base_by_network = {
220
296
  "mainnet": "https://solana-mainnet.g.alchemy.com/v2",
221
- "devnet": "https://solana-devnet.g.alchemy.com/v2",
222
297
  }
223
- alchemy_base = alchemy_base_by_network.get(network.strip().lower())
298
+ alchemy_base = alchemy_base_by_network.get(normalized_network)
224
299
  if alchemy_base:
225
300
  return {
226
301
  "mode": "user_direct",
227
302
  "provider": "alchemy",
228
303
  "transport": "direct",
229
304
  "rpc_urls": resolve_solana_rpc_urls(
230
- network,
305
+ normalized_network,
231
306
  f"{alchemy_base}/{alchemy_key}",
232
307
  "",
233
308
  ),
@@ -237,35 +312,40 @@ def resolve_runtime_solana_rpc_config(
237
312
  if helius_key:
238
313
  helius_base_by_network = {
239
314
  "mainnet": "https://mainnet.helius-rpc.com/",
240
- "devnet": "https://devnet.helius-rpc.com/",
241
315
  }
242
- helius_base = helius_base_by_network.get(network.strip().lower())
316
+ helius_base = helius_base_by_network.get(normalized_network)
243
317
  if helius_base:
244
318
  return {
245
319
  "mode": "user_direct",
246
320
  "provider": "helius",
247
321
  "transport": "direct",
248
322
  "rpc_urls": resolve_solana_rpc_urls(
249
- network,
323
+ normalized_network,
250
324
  f"{helius_base}?api-key={helius_key}",
251
325
  "",
252
326
  ),
253
327
  }
254
328
 
255
- if network.strip().lower() == "mainnet" and (mode == "shared_proxy" or (mode == "auto" and gateway_url)):
329
+ if normalized_network == "mainnet" and (mode == "shared_proxy" or (mode == "auto" and gateway_url)):
256
330
  if gateway_url:
257
331
  return {
258
332
  "mode": "shared_proxy",
259
333
  "provider": gateway_provider,
260
334
  "transport": "proxy",
261
- "rpc_urls": [_build_provider_gateway_rpc_url(gateway_url, gateway_provider, network)],
335
+ "rpc_urls": [
336
+ _build_provider_gateway_rpc_url(
337
+ gateway_url,
338
+ gateway_provider,
339
+ normalized_network,
340
+ )
341
+ ],
262
342
  }
263
343
 
264
344
  return {
265
345
  "mode": "public_fallback",
266
346
  "provider": "official",
267
347
  "transport": "direct",
268
- "rpc_urls": resolve_solana_rpc_urls(network, configured, configured_list),
348
+ "rpc_urls": resolve_solana_rpc_urls(normalized_network, configured, configured_list),
269
349
  }
270
350
 
271
351
 
@@ -289,10 +369,7 @@ def resolve_runtime_solana_swap_config(network: str) -> dict[str, str]:
289
369
  requested = _normalize_swap_provider(
290
370
  os.getenv("SOLANA_SWAP_PROVIDER", settings.solana_swap_provider)
291
371
  )
292
- normalized_network = network.strip().lower()
293
-
294
- if normalized_network != "mainnet":
295
- return {"provider": "jupiter", "transport": "direct"}
372
+ normalize_solana_network(network)
296
373
 
297
374
  if requested == "jupiter":
298
375
  return {"provider": "jupiter", "transport": "direct"}
@@ -14,6 +14,7 @@ from urllib.parse import urlparse
14
14
  from urllib.request import urlopen
15
15
 
16
16
  from agent_wallet.config import (
17
+ normalize_evm_network,
17
18
  resolve_boot_key,
18
19
  resolve_evm_wallet_password,
19
20
  resolve_openclaw_home,
@@ -27,18 +28,7 @@ LOCAL_WDK_EVM_HOSTS = {"127.0.0.1", "localhost", "::1"}
27
28
 
28
29
 
29
30
  def _normalize_evm_network(value: str | None) -> str:
30
- network = str(value or "").strip().lower()
31
- aliases = {
32
- "mainnet": "ethereum",
33
- "eth": "ethereum",
34
- "eth-mainnet": "ethereum",
35
- "base-mainnet": "base",
36
- "base_sepolia": "base-sepolia",
37
- }
38
- network = aliases.get(network, network)
39
- if network not in {"ethereum", "sepolia", "base", "base-sepolia"}:
40
- return "ethereum"
41
- return network
31
+ return normalize_evm_network(value)
42
32
 
43
33
 
44
34
  def _resolve_service_url(service_url: str | None = None) -> str:
@@ -52,8 +42,6 @@ def _paired_network(network: str) -> str | None:
52
42
  mapping = {
53
43
  "ethereum": "base",
54
44
  "base": "ethereum",
55
- "sepolia": "base-sepolia",
56
- "base-sepolia": "sepolia",
57
45
  }
58
46
  return mapping.get(_normalize_evm_network(network))
59
47
 
@@ -101,9 +101,10 @@ class OpenClawWalletAdapter:
101
101
  "eth": "ethereum",
102
102
  "eth-mainnet": "ethereum",
103
103
  "base-mainnet": "base",
104
- "base_sepolia": "base-sepolia",
105
104
  }
106
105
  network = aliases.get(network, network)
106
+ if network in {"sepolia", "base-sepolia", "base_sepolia"}:
107
+ raise WalletBackendError("EVM testnets are no longer supported. Use ethereum or base.")
107
108
  if network not in {"ethereum", "base"}:
108
109
  raise WalletBackendError("EVM network must be 'ethereum' or 'base'.")
109
110
  return network
@@ -2314,6 +2315,25 @@ class OpenClawWalletAdapter:
2314
2315
  read_only=True,
2315
2316
  risk_level="low",
2316
2317
  ),
2318
+ AgentToolSpec(
2319
+ name="get_kamino_open_positions",
2320
+ description=(
2321
+ "Get all open Kamino lending positions for a Solana wallet on mainnet, "
2322
+ "aggregated across markets with loan details, reserve APYs, and rewards."
2323
+ ),
2324
+ input_schema={
2325
+ "type": "object",
2326
+ "properties": {
2327
+ "user": {
2328
+ "type": "string",
2329
+ "description": "Optional Solana wallet address override. If omitted, use the configured wallet.",
2330
+ },
2331
+ },
2332
+ "additionalProperties": False,
2333
+ },
2334
+ read_only=True,
2335
+ risk_level="low",
2336
+ ),
2317
2337
  ]
2318
2338
 
2319
2339
  if capabilities.can_sign_message:
@@ -3221,30 +3241,6 @@ class OpenClawWalletAdapter:
3221
3241
  )
3222
3242
  )
3223
3243
 
3224
- tools.append(
3225
- AgentToolSpec(
3226
- name="request_devnet_airdrop",
3227
- description=(
3228
- "Request SOL from the Solana faucet on devnet or testnet. "
3229
- "Only available outside mainnet."
3230
- ),
3231
- input_schema={
3232
- "type": "object",
3233
- "properties": {
3234
- "amount": {
3235
- "type": "number",
3236
- "description": "Amount of SOL to request from faucet.",
3237
- }
3238
- },
3239
- "required": ["amount"],
3240
- "additionalProperties": False,
3241
- },
3242
- read_only=False,
3243
- requires_explicit_user_intent=True,
3244
- risk_level="low",
3245
- )
3246
- )
3247
-
3248
3244
  tools.extend(self._x402_tool_specs())
3249
3245
  return [tool for tool in tools if tool.name not in TEMPORARILY_DISABLED_TOOLS]
3250
3246
 
@@ -4637,6 +4633,13 @@ class OpenClawWalletAdapter:
4637
4633
  data = await self.backend.get_kamino_lend_user_rewards(user=user)
4638
4634
  return AgentToolResult(tool=tool_name, ok=True, data=data)
4639
4635
 
4636
+ if tool_name == "get_kamino_open_positions":
4637
+ user = args.get("user")
4638
+ if user is not None and not isinstance(user, str):
4639
+ raise WalletBackendError("user must be a string when provided.")
4640
+ data = await self.backend.get_kamino_open_positions(user=user)
4641
+ return AgentToolResult(tool=tool_name, ok=True, data=data)
4642
+
4640
4643
  if tool_name == "sign_wallet_message":
4641
4644
  user_confirmed = args.get("user_confirmed")
4642
4645
  if user_confirmed is not True:
@@ -5199,13 +5202,6 @@ class OpenClawWalletAdapter:
5199
5202
  ),
5200
5203
  )
5201
5204
 
5202
- if tool_name == "request_devnet_airdrop":
5203
- amount = args.get("amount")
5204
- if not isinstance(amount, (int, float)) or amount <= 0:
5205
- raise WalletBackendError("amount must be a positive number.")
5206
- result = await self.backend.request_testnet_airdrop(float(amount))
5207
- return AgentToolResult(tool=tool_name, ok=True, data=result)
5208
-
5209
5205
  if tool_name == "transfer_spl_token":
5210
5206
  recipient = args.get("recipient")
5211
5207
  mint = args.get("mint")
@@ -7,7 +7,7 @@ from typing import Any
7
7
 
8
8
  from agent_wallet.approval import issue_approval_token
9
9
  from agent_wallet.btc_user_wallets import get_user_btc_wallet_binding
10
- from agent_wallet.config import settings
10
+ from agent_wallet.config import normalize_btc_network, normalize_evm_network, settings
11
11
  from agent_wallet.evm_user_wallets import ensure_user_evm_wallet_ready
12
12
  from agent_wallet.models import OpenClawWalletSessionMetadata
13
13
  from agent_wallet.openclaw_adapter import OpenClawWalletAdapter
@@ -105,8 +105,7 @@ def onboard_openclaw_user_wallet(
105
105
  if wdk_btc_account_index is None
106
106
  else int(wdk_btc_account_index)
107
107
  )
108
- requested_network = (network or settings.solana_network).strip().lower() or "bitcoin"
109
- effective_network = "bitcoin" if requested_network == "mainnet" else requested_network
108
+ effective_network = normalize_btc_network(network or settings.solana_network)
110
109
  binding: dict[str, Any] | None = None
111
110
  wallet_id = str(wdk_btc_wallet_id or settings.wdk_btc_wallet_id).strip()
112
111
  if not service_url:
@@ -164,15 +163,7 @@ def onboard_openclaw_user_wallet(
164
163
  if wdk_evm_account_index is None
165
164
  else int(wdk_evm_account_index)
166
165
  )
167
- requested_network = (network or settings.solana_network).strip().lower() or "ethereum"
168
- aliases = {
169
- "mainnet": "ethereum",
170
- "eth": "ethereum",
171
- "eth-mainnet": "ethereum",
172
- "base-mainnet": "base",
173
- "base_sepolia": "base-sepolia",
174
- }
175
- effective_network = aliases.get(requested_network, requested_network)
166
+ effective_network = normalize_evm_network(network or settings.solana_network)
176
167
  wallet_id = str(wdk_evm_wallet_id or settings.wdk_evm_wallet_id).strip()
177
168
  if not service_url:
178
169
  raise WalletBackendError("wdk_evm_service_url is required for backend=wdk_evm_local.")
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
- from agent_wallet.config import settings
7
+ from agent_wallet.config import normalize_solana_network, settings
8
8
  from agent_wallet.exceptions import ProviderError
9
9
  from agent_wallet.http_client import get_client
10
10
 
@@ -42,9 +42,7 @@ def _normalized_tx_response(data: Any, *, provider_name: str) -> dict[str, Any]:
42
42
 
43
43
 
44
44
  def _env_name(network: str) -> str:
45
- normalized = str(network).strip().lower()
46
- if normalized == "devnet":
47
- return "devnet"
45
+ normalize_solana_network(network)
48
46
  return "mainnet-beta"
49
47
 
50
48
 
@@ -125,6 +123,25 @@ async def fetch_lend_user_rewards(*, user: str) -> dict[str, Any]:
125
123
  return data
126
124
 
127
125
 
126
+ async def fetch_lend_loan_info(
127
+ *,
128
+ obligation: str,
129
+ network: str,
130
+ ) -> dict[str, Any]:
131
+ """Fetch a detailed Kamino loan report for one obligation."""
132
+ client = get_client()
133
+ response = await client.get(
134
+ f"{_normalized_api_base()}/klend/loans/{obligation}",
135
+ params={"env": _env_name(network)},
136
+ )
137
+ if response.status_code != 200:
138
+ raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
139
+ data = response.json()
140
+ if not isinstance(data, dict):
141
+ raise ProviderError("kamino", "Unexpected loan info response from Kamino.")
142
+ return data
143
+
144
+
128
145
  async def build_lend_deposit_transaction(
129
146
  *,
130
147
  wallet: str,
@@ -42,11 +42,6 @@ def _parse_gateway_url(rpc_url: str) -> tuple[str, str, str]:
42
42
 
43
43
  def _fallback_for_rpc_url(rpc_url: str) -> str:
44
44
  """Choose a cluster-appropriate official fallback URL."""
45
- lowered = rpc_url.lower()
46
- if "devnet" in lowered:
47
- return "https://api.devnet.solana.com"
48
- if "testnet" in lowered:
49
- return "https://api.testnet.solana.com"
50
45
  return SOLANA_RPC_FALLBACK
51
46
 
52
47
 
@@ -415,24 +410,6 @@ async def simulate_transaction(
415
410
  }
416
411
 
417
412
 
418
- async def request_airdrop(
419
- address: str,
420
- lamports: int,
421
- rpc_url: str,
422
- commitment: str = "confirmed",
423
- ) -> dict[str, Any]:
424
- """Request a devnet or testnet SOL airdrop."""
425
- data = await rpc_call(
426
- "requestAirdrop",
427
- [address, lamports, {"commitment": commitment}],
428
- rpc_url=rpc_url,
429
- )
430
- return {
431
- "signature": data.get("result"),
432
- "source": "solana-rpc",
433
- }
434
-
435
-
436
413
  async def get_signature_status(
437
414
  signature: str,
438
415
  rpc_url: str,
@@ -9,7 +9,7 @@ import logging
9
9
  from typing import Any
10
10
  from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
11
11
 
12
- from agent_wallet.config import resolve_solana_rpc_url
12
+ from agent_wallet.config import normalize_solana_network, resolve_solana_rpc_url
13
13
  from agent_wallet.exceptions import ProviderError
14
14
  from agent_wallet.http_client import get_client
15
15
  from agent_wallet.wallet_layer.base import AgentWalletBackend
@@ -19,13 +19,10 @@ AGENTIC_MARKET_API_BASE_URL = "https://api.agentic.market/v1"
19
19
  X402_EXECUTE_TIMEOUT_SECONDS = 45.0
20
20
  SOLANA_CAIP_BY_NETWORK = {
21
21
  "mainnet": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
22
- "devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
23
22
  }
24
23
  EVM_CAIP_BY_NETWORK = {
25
24
  "ethereum": "eip155:1",
26
25
  "base": "eip155:8453",
27
- "sepolia": "eip155:11155111",
28
- "base-sepolia": "eip155:84532",
29
26
  }
30
27
  _USDC_IDENTIFIERS = {
31
28
  "usdc",
@@ -63,7 +60,7 @@ def _backend_solana_sdk_rpc_url(backend: AgentWalletBackend) -> str | None:
63
60
  if primary.startswith(("http://", "https://")):
64
61
  return primary
65
62
  network = _backend_network(backend)
66
- fallback = resolve_solana_rpc_url(network or "mainnet", "")
63
+ fallback = resolve_solana_rpc_url(normalize_solana_network(network or "mainnet"), "")
67
64
  return _trim(fallback) or None
68
65
 
69
66
 
@@ -361,7 +358,7 @@ def _wallet_caip_networks(backend: AgentWalletBackend) -> list[str]:
361
358
  def _solana_exact_execution_supported(backend: AgentWalletBackend) -> bool:
362
359
  return (
363
360
  _backend_chain(backend) == "solana"
364
- and _backend_network(backend) in {"mainnet", "devnet"}
361
+ and _backend_network(backend) == "mainnet"
365
362
  and getattr(backend, "signer", None) is not None
366
363
  )
367
364
 
@@ -369,7 +366,7 @@ def _solana_exact_execution_supported(backend: AgentWalletBackend) -> bool:
369
366
  def _evm_exact_execution_supported(backend: AgentWalletBackend) -> bool:
370
367
  return (
371
368
  _backend_chain(backend) == "evm"
372
- and _backend_network(backend) in {"base", "base-sepolia"}
369
+ and _backend_network(backend) == "base"
373
370
  and callable(getattr(backend, "sign_x402_evm_exact_typed_data", None))
374
371
  )
375
372
 
@@ -388,9 +385,7 @@ def _wallet_x402_support_summary(backend: AgentWalletBackend) -> dict[str, Any]:
388
385
  supported_networks = _wallet_caip_networks(backend)
389
386
  planned_execution_networks = {
390
387
  "eip155:8453",
391
- "eip155:84532",
392
388
  "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
393
- "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
394
389
  }
395
390
  execution_modes: list[str] = []
396
391
  if _solana_exact_execution_supported(backend):
@@ -14,6 +14,7 @@ from agent_wallet.bootstrap import (
14
14
  )
15
15
  from agent_wallet.config import (
16
16
  allow_plaintext_user_wallet_migration,
17
+ normalize_solana_network,
17
18
  resolve_openclaw_home,
18
19
  resolve_runtime_solana_rpc_config,
19
20
  resolve_runtime_solana_swap_config,
@@ -46,13 +47,13 @@ def normalize_user_id(user_id: str) -> str:
46
47
 
47
48
  def resolve_user_wallet_path(user_id: str, network: str | None = None) -> Path:
48
49
  """Resolve the wallet file path for a given OpenClaw user."""
49
- effective_network = (network or settings.solana_network).strip().lower() or "mainnet"
50
+ effective_network = normalize_solana_network(network or settings.solana_network)
50
51
  user_dir = resolve_openclaw_home() / "users" / normalize_user_id(user_id) / "wallets"
51
52
  return user_dir / f"solana-{effective_network}-agent.json"
52
53
 
53
54
 
54
55
  def _user_wallet_metadata(user_id: str, address: str, network: str | None = None) -> dict[str, str]:
55
- effective_network = (network or settings.solana_network).strip().lower() or "mainnet"
56
+ effective_network = normalize_solana_network(network or settings.solana_network)
56
57
  return {
57
58
  "address": address,
58
59
  "user_id": user_id,
@@ -61,7 +62,7 @@ def _user_wallet_metadata(user_id: str, address: str, network: str | None = None
61
62
 
62
63
 
63
64
  def _resolve_effective_network(network: str | None = None) -> str:
64
- return (network or settings.solana_network).strip().lower() or "mainnet"
65
+ return normalize_solana_network(network or settings.solana_network)
65
66
 
66
67
 
67
68
  def _resolve_user_wallet_master_key(
@@ -520,6 +520,9 @@ class AgentWalletBackend(ABC):
520
520
  async def get_kamino_lend_user_rewards(self, user: str | None = None) -> dict[str, Any]:
521
521
  raise WalletBackendError(f"{self.name} does not support Kamino rewards lookup.")
522
522
 
523
+ async def get_kamino_open_positions(self, user: str | None = None) -> dict[str, Any]:
524
+ raise WalletBackendError(f"{self.name} does not support Kamino open position lookup.")
525
+
523
526
  async def preview_kamino_lend_deposit(
524
527
  self,
525
528
  market: str,
@@ -994,9 +997,6 @@ class AgentWalletBackend(ABC):
994
997
  ),
995
998
  )
996
999
 
997
- async def request_testnet_airdrop(self, amount_native: float) -> dict[str, Any]:
998
- raise WalletBackendError(f"{self.name} does not support testnet airdrops.")
999
-
1000
1000
  async def preview_native_stake(
1001
1001
  self,
1002
1002
  vote_account: str,
@@ -7,6 +7,8 @@ from pathlib import Path
7
7
  from agent_wallet.bootstrap import ensure_solana_wallet_ready, ensure_wallet_pin
8
8
  from agent_wallet.encrypted_storage import load_wallet_secret_material
9
9
  from agent_wallet.config import (
10
+ normalize_btc_network,
11
+ normalize_solana_network,
10
12
  resolve_runtime_solana_rpc_config,
11
13
  resolve_runtime_solana_swap_config,
12
14
  resolve_solana_private_key,
@@ -44,6 +46,7 @@ def create_wallet_backend() -> AgentWalletBackend | None:
44
46
  return None
45
47
 
46
48
  if backend in {"solana", "solana_local", "solana-local"}:
49
+ solana_network = normalize_solana_network(settings.solana_network)
47
50
  secret_material = _load_keypair_material()
48
51
  signer = (
49
52
  SolanaLocalKeypairSigner.from_secret_material(secret_material)
@@ -55,19 +58,19 @@ def create_wallet_backend() -> AgentWalletBackend | None:
55
58
  ensure_wallet_pin(
56
59
  Path(keypair_path).expanduser(),
57
60
  address=signer.address,
58
- network=settings.solana_network,
61
+ network=solana_network,
59
62
  )
60
63
  configured_address = settings.solana_agent_public_key.strip() or None
61
64
  rpc_config = resolve_runtime_solana_rpc_config(
62
- settings.solana_network,
65
+ solana_network,
63
66
  settings.solana_rpc_url,
64
67
  settings.solana_rpc_urls,
65
68
  )
66
- swap_config = resolve_runtime_solana_swap_config(settings.solana_network)
69
+ swap_config = resolve_runtime_solana_swap_config(solana_network)
67
70
  return SolanaWalletBackend(
68
71
  rpc_url=rpc_config["rpc_urls"],
69
72
  commitment=settings.solana_commitment,
70
- network=settings.solana_network,
73
+ network=solana_network,
71
74
  signer=signer,
72
75
  address=configured_address,
73
76
  sign_only=settings.agent_wallet_sign_only,
@@ -82,7 +85,7 @@ def create_wallet_backend() -> AgentWalletBackend | None:
82
85
  return WdkBtcLocalWalletBackend(
83
86
  service_url=settings.wdk_btc_service_url,
84
87
  wallet_id=settings.wdk_btc_wallet_id,
85
- network=settings.solana_network,
88
+ network=normalize_btc_network(settings.solana_network),
86
89
  account_index=settings.wdk_btc_account_index,
87
90
  sign_only=settings.agent_wallet_sign_only,
88
91
  )