@agentlayer.tech/wallet 0.1.27 → 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.
- package/.openclaw/extensions/agent-wallet/README.md +4 -5
- package/.openclaw/extensions/agent-wallet/dist/index.js +31 -31
- package/.openclaw/extensions/agent-wallet/index.ts +31 -31
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +2 -2
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +52 -0
- package/README.md +9 -0
- package/agent-wallet/README.md +18 -22
- package/agent-wallet/agent_wallet/bootstrap.py +28 -12
- package/agent-wallet/agent_wallet/btc_user_wallets.py +2 -7
- package/agent-wallet/agent_wallet/config.py +99 -22
- package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -14
- package/agent-wallet/agent_wallet/openclaw_adapter.py +72 -108
- package/agent-wallet/agent_wallet/openclaw_runtime.py +3 -12
- package/agent-wallet/agent_wallet/providers/kamino.py +21 -4
- package/agent-wallet/agent_wallet/providers/solana_rpc.py +0 -23
- package/agent-wallet/agent_wallet/providers/x402.py +198 -18
- package/agent-wallet/agent_wallet/user_wallets.py +4 -3
- package/agent-wallet/agent_wallet/wallet_layer/base.py +3 -3
- package/agent-wallet/agent_wallet/wallet_layer/factory.py +8 -5
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +437 -44
- package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +2 -8
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +13 -13
- package/agent-wallet/examples/openclaw_runtime_onboarding.py +1 -1
- package/agent-wallet/examples/openclaw_user_wallet_example.py +1 -1
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +2 -1
- package/agent-wallet/scripts/bootstrap_openclaw_btc.py +3 -5
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +2 -12
- package/agent-wallet/scripts/build_release_bundle.py +1 -0
- package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1 -4
- package/agent-wallet/scripts/install_agent_wallet.py +1 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +4 -6
- package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +2 -4
- package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +2 -15
- package/agent-wallet/scripts/reveal_btc_seed.sh +7 -16
- package/agent-wallet/scripts/setup_btc_wallet.sh +7 -16
- package/agent-wallet/scripts/setup_evm_wallet.sh +1 -11
- package/agent-wallet/scripts/switch_openclaw_wallet_network.py +4 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +0 -1
- package/bin/openclaw-agent-wallet.mjs +289 -0
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +20 -0
- package/claude-code/plugins/agent-wallet/.mcp.json +14 -0
- package/claude-code/plugins/agent-wallet/README.md +65 -0
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +34 -0
- package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +38 -0
- package/codex/plugins/agent-wallet/.mcp.json +15 -0
- package/codex/plugins/agent-wallet/README.md +39 -0
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +21 -0
- package/codex/plugins/agent-wallet/server.py +1077 -0
- package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
- package/hermes/plugins/agent_wallet/schemas.py +2 -2
- package/hermes/plugins/agent_wallet/tools.py +17 -3
- package/package.json +6 -1
- package/setup.sh +2 -0
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from agent_wallet.config import refuse_mainnet_wallet_recreation
|
|
8
|
+
from agent_wallet.config import normalize_solana_network, refuse_mainnet_wallet_recreation
|
|
9
9
|
from agent_wallet.file_ops import atomic_write_text
|
|
10
10
|
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
11
11
|
from agent_wallet.wallet_layer.base58 import b58encode
|
|
@@ -79,11 +79,12 @@ def load_wallet_pin(path: Path) -> dict[str, str] | None:
|
|
|
79
79
|
|
|
80
80
|
def write_wallet_pin(path: Path, *, address: str, network: str) -> dict[str, str]:
|
|
81
81
|
"""Persist the expected wallet address for later mismatch checks."""
|
|
82
|
+
normalized_network = normalize_solana_network(network)
|
|
82
83
|
payload = {
|
|
83
84
|
"kind": WALLET_ADDRESS_PIN_KIND,
|
|
84
85
|
"version": WALLET_ADDRESS_PIN_VERSION,
|
|
85
86
|
"address": address,
|
|
86
|
-
"network":
|
|
87
|
+
"network": normalized_network,
|
|
87
88
|
"wallet_file": path.name,
|
|
88
89
|
}
|
|
89
90
|
pin_path = resolve_wallet_pin_path(path)
|
|
@@ -97,7 +98,7 @@ def write_wallet_pin(path: Path, *, address: str, network: str) -> dict[str, str
|
|
|
97
98
|
|
|
98
99
|
def ensure_wallet_pin(path: Path, *, address: str, network: str) -> dict[str, str]:
|
|
99
100
|
"""Ensure the wallet pin exists and matches the expected address."""
|
|
100
|
-
expected_network = network
|
|
101
|
+
expected_network = normalize_solana_network(network)
|
|
101
102
|
existing = load_wallet_pin(path)
|
|
102
103
|
if existing is None:
|
|
103
104
|
return write_wallet_pin(path, address=address, network=expected_network)
|
|
@@ -114,7 +115,7 @@ def ensure_wallet_pin(path: Path, *, address: str, network: str) -> dict[str, st
|
|
|
114
115
|
|
|
115
116
|
def refuse_recreation_if_pinned(path: Path, *, network: str) -> None:
|
|
116
117
|
"""Refuse to recreate a wallet when a mainnet address is already pinned."""
|
|
117
|
-
expected_network = network
|
|
118
|
+
expected_network = normalize_solana_network(network)
|
|
118
119
|
if expected_network != "mainnet" or not refuse_mainnet_wallet_recreation():
|
|
119
120
|
return
|
|
120
121
|
existing = load_wallet_pin(path)
|
|
@@ -128,7 +129,11 @@ def refuse_recreation_if_pinned(path: Path, *, network: str) -> None:
|
|
|
128
129
|
|
|
129
130
|
def ensure_solana_wallet_ready() -> dict[str, str] | None:
|
|
130
131
|
"""Ensure that a Solana wallet exists when auto-create is enabled."""
|
|
131
|
-
from agent_wallet.config import
|
|
132
|
+
from agent_wallet.config import (
|
|
133
|
+
default_solana_wallet_path,
|
|
134
|
+
resolve_solana_private_key,
|
|
135
|
+
settings,
|
|
136
|
+
)
|
|
132
137
|
|
|
133
138
|
if settings.agent_wallet_backend.strip().lower() not in {"solana", "solana_local", "solana-local"}:
|
|
134
139
|
return None
|
|
@@ -136,8 +141,13 @@ def ensure_solana_wallet_ready() -> dict[str, str] | None:
|
|
|
136
141
|
if resolve_solana_private_key():
|
|
137
142
|
return {"address": "", "path": ""}
|
|
138
143
|
|
|
144
|
+
normalized_network = normalize_solana_network(settings.solana_network)
|
|
139
145
|
configured_path = settings.solana_agent_keypair_path.strip()
|
|
140
|
-
path =
|
|
146
|
+
path = (
|
|
147
|
+
Path(configured_path).expanduser()
|
|
148
|
+
if configured_path
|
|
149
|
+
else default_solana_wallet_path(normalized_network)
|
|
150
|
+
)
|
|
141
151
|
|
|
142
152
|
if path.exists():
|
|
143
153
|
return {"address": "", "path": str(path)}
|
|
@@ -145,9 +155,9 @@ def ensure_solana_wallet_ready() -> dict[str, str] | None:
|
|
|
145
155
|
if not settings.solana_auto_create_wallet:
|
|
146
156
|
return None
|
|
147
157
|
|
|
148
|
-
refuse_recreation_if_pinned(path, network=
|
|
158
|
+
refuse_recreation_if_pinned(path, network=normalized_network)
|
|
149
159
|
created = create_solana_wallet_file(path)
|
|
150
|
-
write_wallet_pin(path, address=created["address"], network=
|
|
160
|
+
write_wallet_pin(path, address=created["address"], network=normalized_network)
|
|
151
161
|
return created
|
|
152
162
|
|
|
153
163
|
|
|
@@ -155,22 +165,28 @@ def describe_bootstrap() -> dict[str, str | bool]:
|
|
|
155
165
|
"""Return the effective bootstrap configuration for installer/runtime usage."""
|
|
156
166
|
from agent_wallet.config import (
|
|
157
167
|
default_solana_wallet_path,
|
|
168
|
+
normalize_solana_network,
|
|
158
169
|
resolve_solana_rpc_url,
|
|
159
170
|
resolve_runtime_solana_rpc_urls,
|
|
160
171
|
settings,
|
|
161
172
|
)
|
|
162
173
|
|
|
174
|
+
normalized_network = normalize_solana_network(settings.solana_network)
|
|
163
175
|
configured_path = settings.solana_agent_keypair_path.strip()
|
|
164
|
-
path =
|
|
176
|
+
path = (
|
|
177
|
+
Path(configured_path).expanduser()
|
|
178
|
+
if configured_path
|
|
179
|
+
else default_solana_wallet_path(normalized_network)
|
|
180
|
+
)
|
|
165
181
|
rpc_urls = resolve_runtime_solana_rpc_urls(
|
|
166
|
-
|
|
182
|
+
normalized_network,
|
|
167
183
|
settings.solana_rpc_url,
|
|
168
184
|
settings.solana_rpc_urls,
|
|
169
185
|
)
|
|
170
186
|
return {
|
|
171
187
|
"backend": settings.agent_wallet_backend,
|
|
172
|
-
"network":
|
|
173
|
-
"rpc_url": rpc_urls[0] if rpc_urls else resolve_solana_rpc_url(
|
|
188
|
+
"network": normalized_network,
|
|
189
|
+
"rpc_url": rpc_urls[0] if rpc_urls else resolve_solana_rpc_url(normalized_network, ""),
|
|
174
190
|
"rpc_urls": rpc_urls,
|
|
175
191
|
"auto_create_wallet": settings.solana_auto_create_wallet,
|
|
176
192
|
"keypair_path": str(path),
|
|
@@ -6,19 +6,14 @@ import json
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
from agent_wallet.config import resolve_openclaw_home, settings
|
|
9
|
+
from agent_wallet.config import normalize_btc_network, resolve_openclaw_home, settings
|
|
10
10
|
from agent_wallet.providers.wdk_btc_local import WdkBtcLocalClient
|
|
11
11
|
from agent_wallet.user_wallets import normalize_user_id
|
|
12
12
|
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _normalize_btc_network(value: str | None) -> str:
|
|
16
|
-
|
|
17
|
-
aliases = {"mainnet": "bitcoin"}
|
|
18
|
-
network = aliases.get(network, network)
|
|
19
|
-
if network not in {"bitcoin", "testnet", "regtest"}:
|
|
20
|
-
return "bitcoin"
|
|
21
|
-
return network
|
|
16
|
+
return normalize_btc_network(value)
|
|
22
17
|
|
|
23
18
|
|
|
24
19
|
def _resolve_service_url(service_url: str | None = None) -> str:
|
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
323
|
+
normalized_network,
|
|
250
324
|
f"{helius_base}?api-key={helius_key}",
|
|
251
325
|
"",
|
|
252
326
|
),
|
|
253
327
|
}
|
|
254
328
|
|
|
255
|
-
if
|
|
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": [
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -245,8 +246,9 @@ class OpenClawWalletAdapter:
|
|
|
245
246
|
AgentToolSpec(
|
|
246
247
|
name="x402_pay_request",
|
|
247
248
|
description=(
|
|
248
|
-
"
|
|
249
|
-
"
|
|
249
|
+
"Pay for and call an x402 endpoint using the active wallet backend. "
|
|
250
|
+
"The tool probes the endpoint, validates compatibility, signs the payment, "
|
|
251
|
+
"and returns the service response in one call."
|
|
250
252
|
),
|
|
251
253
|
input_schema={
|
|
252
254
|
"type": "object",
|
|
@@ -257,26 +259,13 @@ class OpenClawWalletAdapter:
|
|
|
257
259
|
"query": {"type": "object", "additionalProperties": True},
|
|
258
260
|
"json_body": {},
|
|
259
261
|
"text_body": {"type": "string"},
|
|
260
|
-
"mode": {
|
|
261
|
-
"type": "string",
|
|
262
|
-
"enum": ["prepare", "execute"],
|
|
263
|
-
"description": "prepare validates the payment plan; execute sends the paid retry.",
|
|
264
|
-
},
|
|
265
262
|
"purpose": {"type": "string"},
|
|
266
|
-
"user_intent": {
|
|
267
|
-
"type": "boolean",
|
|
268
|
-
"description": "Must be true for prepare mode.",
|
|
269
|
-
},
|
|
270
|
-
"approval_token": {
|
|
271
|
-
"type": "string",
|
|
272
|
-
"description": "Required for execute mode and must be issued against the exact x402 payment summary.",
|
|
273
|
-
},
|
|
274
263
|
},
|
|
275
|
-
"required": ["url", "
|
|
264
|
+
"required": ["url", "purpose"],
|
|
276
265
|
"additionalProperties": False,
|
|
277
266
|
},
|
|
278
267
|
read_only=False,
|
|
279
|
-
requires_explicit_user_intent=
|
|
268
|
+
requires_explicit_user_intent=False,
|
|
280
269
|
risk_level="high",
|
|
281
270
|
),
|
|
282
271
|
]
|
|
@@ -872,6 +861,42 @@ class OpenClawWalletAdapter:
|
|
|
872
861
|
)
|
|
873
862
|
return annotated
|
|
874
863
|
|
|
864
|
+
def _annotate_x402_payload(
|
|
865
|
+
self,
|
|
866
|
+
payload: dict[str, Any],
|
|
867
|
+
*,
|
|
868
|
+
mode: str,
|
|
869
|
+
) -> dict[str, Any]:
|
|
870
|
+
if not payload.get("payment_required"):
|
|
871
|
+
return dict(payload)
|
|
872
|
+
annotated = self._annotate_sensitive_payload(
|
|
873
|
+
payload,
|
|
874
|
+
action_label="x402 paid request",
|
|
875
|
+
mode=mode,
|
|
876
|
+
)
|
|
877
|
+
summary = dict(annotated.get("confirmation_summary") or {})
|
|
878
|
+
if summary:
|
|
879
|
+
annotated["payment_summary"] = summary
|
|
880
|
+
requirements = dict(annotated.get("confirmation_requirements") or {})
|
|
881
|
+
requirements["prepare_requires_user_intent"] = False
|
|
882
|
+
requirements["execute_requires_approval_token"] = False
|
|
883
|
+
requirements["execute_requires_mainnet_confirmed_in_token"] = False
|
|
884
|
+
annotated.pop("approval_hint", None)
|
|
885
|
+
if mode == "preview":
|
|
886
|
+
annotated.pop("confirmation_summary", None)
|
|
887
|
+
annotated.pop("confirmation_requirements", None)
|
|
888
|
+
if annotated.get("is_mainnet"):
|
|
889
|
+
annotated["preview_note"] = (
|
|
890
|
+
"This is a paid mainnet endpoint preview only. Review the service URL, network, asset, amount, and payment destination before calling x402_pay_request."
|
|
891
|
+
)
|
|
892
|
+
return annotated
|
|
893
|
+
annotated["confirmation_requirements"] = requirements
|
|
894
|
+
if annotated.get("is_mainnet"):
|
|
895
|
+
annotated["mainnet_warning"] = (
|
|
896
|
+
"Mainnet x402 payment. Confirm the service URL, network, asset, amount, and payment destination before paying."
|
|
897
|
+
)
|
|
898
|
+
return annotated
|
|
899
|
+
|
|
875
900
|
def list_tools(self) -> list[AgentToolSpec]:
|
|
876
901
|
"""Return wallet tools suitable for agent registration."""
|
|
877
902
|
capabilities = self.backend.get_capabilities()
|
|
@@ -2290,6 +2315,25 @@ class OpenClawWalletAdapter:
|
|
|
2290
2315
|
read_only=True,
|
|
2291
2316
|
risk_level="low",
|
|
2292
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
|
+
),
|
|
2293
2337
|
]
|
|
2294
2338
|
|
|
2295
2339
|
if capabilities.can_sign_message:
|
|
@@ -3197,30 +3241,6 @@ class OpenClawWalletAdapter:
|
|
|
3197
3241
|
)
|
|
3198
3242
|
)
|
|
3199
3243
|
|
|
3200
|
-
tools.append(
|
|
3201
|
-
AgentToolSpec(
|
|
3202
|
-
name="request_devnet_airdrop",
|
|
3203
|
-
description=(
|
|
3204
|
-
"Request SOL from the Solana faucet on devnet or testnet. "
|
|
3205
|
-
"Only available outside mainnet."
|
|
3206
|
-
),
|
|
3207
|
-
input_schema={
|
|
3208
|
-
"type": "object",
|
|
3209
|
-
"properties": {
|
|
3210
|
-
"amount": {
|
|
3211
|
-
"type": "number",
|
|
3212
|
-
"description": "Amount of SOL to request from faucet.",
|
|
3213
|
-
}
|
|
3214
|
-
},
|
|
3215
|
-
"required": ["amount"],
|
|
3216
|
-
"additionalProperties": False,
|
|
3217
|
-
},
|
|
3218
|
-
read_only=False,
|
|
3219
|
-
requires_explicit_user_intent=True,
|
|
3220
|
-
risk_level="low",
|
|
3221
|
-
)
|
|
3222
|
-
)
|
|
3223
|
-
|
|
3224
3244
|
tools.extend(self._x402_tool_specs())
|
|
3225
3245
|
return [tool for tool in tools if tool.name not in TEMPORARILY_DISABLED_TOOLS]
|
|
3226
3246
|
|
|
@@ -3309,14 +3329,7 @@ class OpenClawWalletAdapter:
|
|
|
3309
3329
|
text_body=text_body,
|
|
3310
3330
|
)
|
|
3311
3331
|
if data.get("payment_required"):
|
|
3312
|
-
data = self.
|
|
3313
|
-
data,
|
|
3314
|
-
action_label="x402 paid request",
|
|
3315
|
-
mode="preview",
|
|
3316
|
-
)
|
|
3317
|
-
approval_hint = dict(data.get("approval_hint") or {})
|
|
3318
|
-
approval_hint["tool_name"] = "x402_pay_request"
|
|
3319
|
-
data["approval_hint"] = approval_hint
|
|
3332
|
+
data = self._annotate_x402_payload(data, mode="preview")
|
|
3320
3333
|
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
3321
3334
|
|
|
3322
3335
|
if tool_name == "x402_pay_request":
|
|
@@ -3326,10 +3339,7 @@ class OpenClawWalletAdapter:
|
|
|
3326
3339
|
query = args.get("query")
|
|
3327
3340
|
json_body = args.get("json_body")
|
|
3328
3341
|
text_body = args.get("text_body")
|
|
3329
|
-
mode = str(args.get("mode") or "").strip().lower()
|
|
3330
3342
|
purpose = args.get("purpose")
|
|
3331
|
-
user_intent = args.get("user_intent")
|
|
3332
|
-
approval_token = args.get("approval_token")
|
|
3333
3343
|
if not isinstance(url, str) or not url.strip():
|
|
3334
3344
|
raise WalletBackendError("url is required.")
|
|
3335
3345
|
if method is not None and not isinstance(method, str):
|
|
@@ -3340,51 +3350,9 @@ class OpenClawWalletAdapter:
|
|
|
3340
3350
|
raise WalletBackendError("query must be an object when provided.")
|
|
3341
3351
|
if text_body is not None and not isinstance(text_body, str):
|
|
3342
3352
|
raise WalletBackendError("text_body must be a string when provided.")
|
|
3343
|
-
if mode not in {"prepare", "execute"}:
|
|
3344
|
-
raise WalletBackendError("mode must be 'prepare' or 'execute'.")
|
|
3345
3353
|
if not isinstance(purpose, str) or not purpose.strip():
|
|
3346
3354
|
raise WalletBackendError("purpose is required.")
|
|
3347
|
-
|
|
3348
|
-
self._require_prepare_intent(user_intent)
|
|
3349
|
-
data = await x402.prepare_request(
|
|
3350
|
-
backend=active_backend,
|
|
3351
|
-
url=url.strip(),
|
|
3352
|
-
method=method,
|
|
3353
|
-
headers=headers,
|
|
3354
|
-
query=query,
|
|
3355
|
-
json_body=json_body,
|
|
3356
|
-
text_body=text_body,
|
|
3357
|
-
)
|
|
3358
|
-
data["purpose"] = purpose.strip()
|
|
3359
|
-
data = self._annotate_sensitive_payload(
|
|
3360
|
-
data,
|
|
3361
|
-
action_label="x402 paid request",
|
|
3362
|
-
mode="prepare",
|
|
3363
|
-
)
|
|
3364
|
-
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
3365
|
-
preview = await x402.prepare_request(
|
|
3366
|
-
backend=active_backend,
|
|
3367
|
-
url=url.strip(),
|
|
3368
|
-
method=method,
|
|
3369
|
-
headers=headers,
|
|
3370
|
-
query=query,
|
|
3371
|
-
json_body=json_body,
|
|
3372
|
-
text_body=text_body,
|
|
3373
|
-
)
|
|
3374
|
-
preview["purpose"] = purpose.strip()
|
|
3375
|
-
preview = self._annotate_sensitive_payload(
|
|
3376
|
-
preview,
|
|
3377
|
-
action_label="x402 paid request",
|
|
3378
|
-
mode="execute",
|
|
3379
|
-
)
|
|
3380
|
-
self._require_execute_approval(
|
|
3381
|
-
approval_token=approval_token,
|
|
3382
|
-
tool_name=tool_name,
|
|
3383
|
-
summary=preview["confirmation_summary"],
|
|
3384
|
-
action_label="x402 paid request",
|
|
3385
|
-
backend=active_backend,
|
|
3386
|
-
)
|
|
3387
|
-
data = await x402.execute_request(
|
|
3355
|
+
data = await x402.pay_and_fetch(
|
|
3388
3356
|
backend=active_backend,
|
|
3389
3357
|
url=url.strip(),
|
|
3390
3358
|
method=method,
|
|
@@ -3394,11 +3362,7 @@ class OpenClawWalletAdapter:
|
|
|
3394
3362
|
text_body=text_body,
|
|
3395
3363
|
)
|
|
3396
3364
|
data["purpose"] = purpose.strip()
|
|
3397
|
-
data = self.
|
|
3398
|
-
data,
|
|
3399
|
-
action_label="x402 paid request",
|
|
3400
|
-
mode="execute",
|
|
3401
|
-
)
|
|
3365
|
+
data = self._annotate_x402_payload(data, mode="execute")
|
|
3402
3366
|
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
3403
3367
|
|
|
3404
3368
|
if tool_name == "get_wallet_capabilities":
|
|
@@ -4669,6 +4633,13 @@ class OpenClawWalletAdapter:
|
|
|
4669
4633
|
data = await self.backend.get_kamino_lend_user_rewards(user=user)
|
|
4670
4634
|
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
4671
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
|
+
|
|
4672
4643
|
if tool_name == "sign_wallet_message":
|
|
4673
4644
|
user_confirmed = args.get("user_confirmed")
|
|
4674
4645
|
if user_confirmed is not True:
|
|
@@ -5231,13 +5202,6 @@ class OpenClawWalletAdapter:
|
|
|
5231
5202
|
),
|
|
5232
5203
|
)
|
|
5233
5204
|
|
|
5234
|
-
if tool_name == "request_devnet_airdrop":
|
|
5235
|
-
amount = args.get("amount")
|
|
5236
|
-
if not isinstance(amount, (int, float)) or amount <= 0:
|
|
5237
|
-
raise WalletBackendError("amount must be a positive number.")
|
|
5238
|
-
result = await self.backend.request_testnet_airdrop(float(amount))
|
|
5239
|
-
return AgentToolResult(tool=tool_name, ok=True, data=result)
|
|
5240
|
-
|
|
5241
5205
|
if tool_name == "transfer_spl_token":
|
|
5242
5206
|
recipient = args.get("recipient")
|
|
5243
5207
|
mint = args.get("mint")
|