@agentlayer.tech/wallet 0.1.0
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/AGENTS.md +98 -0
- package/.openclaw/extensions/agent-wallet/README.md +127 -0
- package/.openclaw/extensions/agent-wallet/index.ts +1520 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +184 -0
- package/.openclaw/extensions/agent-wallet/package.json +11 -0
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +20 -0
- package/CHANGELOG.md +42 -0
- package/LICENSE +104 -0
- package/README.md +332 -0
- package/RELEASING.md +204 -0
- package/agent-wallet/.env.example +62 -0
- package/agent-wallet/AGENTS.md +129 -0
- package/agent-wallet/README.md +527 -0
- package/agent-wallet/agent_wallet/__init__.py +11 -0
- package/agent-wallet/agent_wallet/approval.py +161 -0
- package/agent-wallet/agent_wallet/bootstrap.py +178 -0
- package/agent-wallet/agent_wallet/btc_user_wallets.py +217 -0
- package/agent-wallet/agent_wallet/config.py +382 -0
- package/agent-wallet/agent_wallet/encrypted_storage.py +161 -0
- package/agent-wallet/agent_wallet/evm_user_wallets.py +370 -0
- package/agent-wallet/agent_wallet/exceptions.py +9 -0
- package/agent-wallet/agent_wallet/file_ops.py +34 -0
- package/agent-wallet/agent_wallet/http_client.py +25 -0
- package/agent-wallet/agent_wallet/models.py +66 -0
- package/agent-wallet/agent_wallet/nonce_registry.py +59 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +5128 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +626 -0
- package/agent-wallet/agent_wallet/openclaw_runtime.py +272 -0
- package/agent-wallet/agent_wallet/plugin_bundle.py +42 -0
- package/agent-wallet/agent_wallet/providers/__init__.py +1 -0
- package/agent-wallet/agent_wallet/providers/bags.py +259 -0
- package/agent-wallet/agent_wallet/providers/evm_portfolio.py +470 -0
- package/agent-wallet/agent_wallet/providers/jupiter.py +567 -0
- package/agent-wallet/agent_wallet/providers/kamino.py +215 -0
- package/agent-wallet/agent_wallet/providers/lifi.py +277 -0
- package/agent-wallet/agent_wallet/providers/solana_rpc.py +470 -0
- package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +114 -0
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +205 -0
- package/agent-wallet/agent_wallet/sealed_keys.py +61 -0
- package/agent-wallet/agent_wallet/solana_stake.py +103 -0
- package/agent-wallet/agent_wallet/solana_tx.py +93 -0
- package/agent-wallet/agent_wallet/spending_limits.py +101 -0
- package/agent-wallet/agent_wallet/transaction_policy.py +518 -0
- package/agent-wallet/agent_wallet/user_wallets.py +355 -0
- package/agent-wallet/agent_wallet/validation.py +31 -0
- package/agent-wallet/agent_wallet/wallet_layer/__init__.py +1 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +808 -0
- package/agent-wallet/agent_wallet/wallet_layer/base58.py +44 -0
- package/agent-wallet/agent_wallet/wallet_layer/factory.py +102 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +4252 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +272 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +1628 -0
- package/agent-wallet/examples/bootstrap_wallet.py +21 -0
- package/agent-wallet/examples/openclaw_runtime_onboarding.py +28 -0
- package/agent-wallet/examples/openclaw_user_wallet_example.py +31 -0
- package/agent-wallet/examples/openclaw_wallet_adapter_example.py +33 -0
- package/agent-wallet/openclaw.plugin.json +138 -0
- package/agent-wallet/pyproject.toml +31 -0
- package/agent-wallet/scripts/bootstrap_openclaw_btc.py +278 -0
- package/agent-wallet/scripts/build_release_bundle.py +188 -0
- package/agent-wallet/scripts/finalize_openclaw_local_wallet_config.py +121 -0
- package/agent-wallet/scripts/install_agent_wallet.py +505 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +226 -0
- package/agent-wallet/scripts/install_openclaw_sealed_keys.py +105 -0
- package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +244 -0
- package/agent-wallet/scripts/reveal_btc_seed.sh +130 -0
- package/agent-wallet/scripts/security_utils.py +37 -0
- package/agent-wallet/scripts/setup_btc_wallet.sh +146 -0
- package/agent-wallet/scripts/switch_openclaw_wallet_network.py +106 -0
- package/agent-wallet/skills/wallet-operator/SKILL.md +128 -0
- package/bin/openclaw-agent-wallet.mjs +487 -0
- package/install-from-github.sh +134 -0
- package/package.json +61 -0
- package/setup.sh +40 -0
- package/wdk-btc-wallet/README.md +325 -0
- package/wdk-btc-wallet/bootstrap.sh +22 -0
- package/wdk-btc-wallet/package-lock.json +1839 -0
- package/wdk-btc-wallet/package.json +18 -0
- package/wdk-btc-wallet/run-local.sh +21 -0
- package/wdk-btc-wallet/src/config.js +160 -0
- package/wdk-btc-wallet/src/json.js +35 -0
- package/wdk-btc-wallet/src/local_vault.js +432 -0
- package/wdk-btc-wallet/src/network_state.js +84 -0
- package/wdk-btc-wallet/src/server.js +257 -0
- package/wdk-btc-wallet/src/wdk_btc_wallet.js +332 -0
- package/wdk-evm-wallet/README.md +183 -0
- package/wdk-evm-wallet/bootstrap.sh +8 -0
- package/wdk-evm-wallet/package-lock.json +2340 -0
- package/wdk-evm-wallet/package.json +23 -0
- package/wdk-evm-wallet/run-local.sh +12 -0
- package/wdk-evm-wallet/src/config.js +274 -0
- package/wdk-evm-wallet/src/json.js +35 -0
- package/wdk-evm-wallet/src/local_vault.js +430 -0
- package/wdk-evm-wallet/src/network_state.js +92 -0
- package/wdk-evm-wallet/src/server.js +575 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +4981 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Host-side helpers for binding local EVM wallets to OpenClaw users."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agent_wallet.config import resolve_openclaw_home, settings
|
|
10
|
+
from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient
|
|
11
|
+
from agent_wallet.user_wallets import normalize_user_id
|
|
12
|
+
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalize_evm_network(value: str | None) -> str:
|
|
16
|
+
network = str(value or "").strip().lower()
|
|
17
|
+
aliases = {
|
|
18
|
+
"mainnet": "ethereum",
|
|
19
|
+
"eth": "ethereum",
|
|
20
|
+
"eth-mainnet": "ethereum",
|
|
21
|
+
"base-mainnet": "base",
|
|
22
|
+
"base_sepolia": "base-sepolia",
|
|
23
|
+
}
|
|
24
|
+
network = aliases.get(network, network)
|
|
25
|
+
if network not in {"ethereum", "sepolia", "base", "base-sepolia"}:
|
|
26
|
+
return "ethereum"
|
|
27
|
+
return network
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve_service_url(service_url: str | None = None) -> str:
|
|
31
|
+
effective = (service_url or settings.wdk_evm_service_url).strip()
|
|
32
|
+
if not effective:
|
|
33
|
+
raise WalletBackendError("wdk_evm_service_url is required for EVM wallet host operations.")
|
|
34
|
+
return effective
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_user_evm_wallet_dir(user_id: str) -> Path:
|
|
38
|
+
return resolve_openclaw_home() / "users" / normalize_user_id(user_id) / "wallets"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_user_evm_wallet_path(user_id: str, network: str | None = None) -> Path:
|
|
42
|
+
effective_network = _normalize_evm_network(network or settings.solana_network)
|
|
43
|
+
user_dir = _resolve_user_evm_wallet_dir(user_id)
|
|
44
|
+
return user_dir / f"evm-{effective_network}-agent.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _write_wallet_binding(path: Path, payload: dict[str, Any]) -> None:
|
|
48
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_user_evm_wallet_binding(user_id: str, network: str | None = None) -> dict[str, Any]:
|
|
53
|
+
path = resolve_user_evm_wallet_path(user_id, network=network)
|
|
54
|
+
if not path.exists():
|
|
55
|
+
raise WalletBackendError(f"EVM wallet binding does not exist yet: {path}")
|
|
56
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
57
|
+
if not isinstance(payload, dict) or not str(payload.get("wallet_id") or "").strip():
|
|
58
|
+
raise WalletBackendError(f"EVM wallet binding is invalid: {path}")
|
|
59
|
+
return payload
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def resolve_user_evm_wallet_binding(
|
|
63
|
+
user_id: str,
|
|
64
|
+
*,
|
|
65
|
+
network: str | None = None,
|
|
66
|
+
service_url: str | None = None,
|
|
67
|
+
wallet_id: str | None = None,
|
|
68
|
+
account_index: int | None = None,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
effective_network = _normalize_evm_network(network or settings.solana_network)
|
|
71
|
+
explicit_wallet_id = str(wallet_id or "").strip()
|
|
72
|
+
if explicit_wallet_id:
|
|
73
|
+
return ensure_user_evm_wallet_binding(
|
|
74
|
+
user_id,
|
|
75
|
+
network=effective_network,
|
|
76
|
+
service_url=service_url,
|
|
77
|
+
wallet_id=explicit_wallet_id,
|
|
78
|
+
account_index=account_index,
|
|
79
|
+
)
|
|
80
|
+
return get_user_evm_wallet_binding(user_id, network=effective_network)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def list_user_evm_wallet_bindings(user_id: str) -> list[dict[str, Any]]:
|
|
84
|
+
user_dir = _resolve_user_evm_wallet_dir(user_id)
|
|
85
|
+
if not user_dir.exists():
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
bindings: list[dict[str, Any]] = []
|
|
89
|
+
for path in sorted(user_dir.glob("evm-*-agent.json")):
|
|
90
|
+
try:
|
|
91
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
92
|
+
except Exception:
|
|
93
|
+
continue
|
|
94
|
+
wallet_id = str(payload.get("wallet_id") or "").strip()
|
|
95
|
+
if not wallet_id:
|
|
96
|
+
continue
|
|
97
|
+
bindings.append(payload)
|
|
98
|
+
return bindings
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def bind_user_evm_wallet(
|
|
102
|
+
user_id: str,
|
|
103
|
+
*,
|
|
104
|
+
wallet_id: str,
|
|
105
|
+
network: str | None = None,
|
|
106
|
+
service_url: str | None = None,
|
|
107
|
+
account_index: int | None = None,
|
|
108
|
+
tolerate_locked: bool = False,
|
|
109
|
+
fallback_address: str | None = None,
|
|
110
|
+
) -> dict[str, Any]:
|
|
111
|
+
effective_network = _normalize_evm_network(network or settings.solana_network)
|
|
112
|
+
effective_account_index = settings.wdk_evm_account_index if account_index is None else int(account_index)
|
|
113
|
+
effective_wallet_id = str(wallet_id or "").strip()
|
|
114
|
+
if not effective_wallet_id:
|
|
115
|
+
raise WalletBackendError("wallet_id is required for EVM wallet binding.")
|
|
116
|
+
|
|
117
|
+
client = WdkEvmLocalClient(_resolve_service_url(service_url))
|
|
118
|
+
wallet_meta = client.post_sync("/v1/evm/wallets/get", {"walletId": effective_wallet_id})
|
|
119
|
+
resolved_address = str(fallback_address or "").strip()
|
|
120
|
+
try:
|
|
121
|
+
address = client.post_sync(
|
|
122
|
+
"/v1/evm/address/resolve",
|
|
123
|
+
{
|
|
124
|
+
"walletId": effective_wallet_id,
|
|
125
|
+
"accountIndex": effective_account_index,
|
|
126
|
+
"network": effective_network,
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
except WalletBackendError as exc:
|
|
130
|
+
is_locked = exc.code == "wallet_locked" or "wallet is locked" in str(exc).strip().lower()
|
|
131
|
+
if not (tolerate_locked and is_locked):
|
|
132
|
+
raise
|
|
133
|
+
else:
|
|
134
|
+
resolved_address = str(address.get("address") or "").strip()
|
|
135
|
+
binding = {
|
|
136
|
+
"user_id": user_id,
|
|
137
|
+
"wallet_id": effective_wallet_id,
|
|
138
|
+
"label": str(wallet_meta.get("label") or "Agent EVM Wallet"),
|
|
139
|
+
"network": effective_network,
|
|
140
|
+
"account_index": effective_account_index,
|
|
141
|
+
"address": resolved_address,
|
|
142
|
+
"storage_format": "local_vault",
|
|
143
|
+
"service_kind": "wdk-evm-wallet",
|
|
144
|
+
"created_at": wallet_meta.get("createdAt"),
|
|
145
|
+
"updated_at": wallet_meta.get("updatedAt"),
|
|
146
|
+
}
|
|
147
|
+
_write_wallet_binding(resolve_user_evm_wallet_path(user_id, effective_network), binding)
|
|
148
|
+
return binding
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def ensure_user_evm_wallet_binding(
|
|
152
|
+
user_id: str,
|
|
153
|
+
*,
|
|
154
|
+
network: str | None = None,
|
|
155
|
+
service_url: str | None = None,
|
|
156
|
+
wallet_id: str | None = None,
|
|
157
|
+
account_index: int | None = None,
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
effective_network = _normalize_evm_network(network or settings.solana_network)
|
|
160
|
+
path = resolve_user_evm_wallet_path(user_id, network=effective_network)
|
|
161
|
+
explicit_wallet_id = str(wallet_id or "").strip()
|
|
162
|
+
if path.exists():
|
|
163
|
+
existing = get_user_evm_wallet_binding(user_id, network=effective_network)
|
|
164
|
+
if explicit_wallet_id and str(existing.get("wallet_id") or "").strip() != explicit_wallet_id:
|
|
165
|
+
return bind_user_evm_wallet(
|
|
166
|
+
user_id,
|
|
167
|
+
wallet_id=explicit_wallet_id,
|
|
168
|
+
network=effective_network,
|
|
169
|
+
service_url=service_url,
|
|
170
|
+
account_index=account_index,
|
|
171
|
+
tolerate_locked=True,
|
|
172
|
+
fallback_address=str(existing.get("address") or "").strip() or None,
|
|
173
|
+
)
|
|
174
|
+
return existing
|
|
175
|
+
|
|
176
|
+
if explicit_wallet_id:
|
|
177
|
+
return bind_user_evm_wallet(
|
|
178
|
+
user_id,
|
|
179
|
+
wallet_id=explicit_wallet_id,
|
|
180
|
+
network=effective_network,
|
|
181
|
+
service_url=service_url,
|
|
182
|
+
account_index=account_index,
|
|
183
|
+
tolerate_locked=True,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
bindings = list_user_evm_wallet_bindings(user_id)
|
|
187
|
+
if not bindings:
|
|
188
|
+
raise WalletBackendError(f"EVM wallet binding does not exist yet: {path}")
|
|
189
|
+
|
|
190
|
+
wallet_ids = {
|
|
191
|
+
str(binding.get("wallet_id") or "").strip()
|
|
192
|
+
for binding in bindings
|
|
193
|
+
if str(binding.get("wallet_id") or "").strip()
|
|
194
|
+
}
|
|
195
|
+
if not wallet_ids:
|
|
196
|
+
raise WalletBackendError(f"EVM wallet binding does not exist yet: {path}")
|
|
197
|
+
if len(wallet_ids) > 1:
|
|
198
|
+
raise WalletBackendError(
|
|
199
|
+
"Multiple EVM wallet bindings exist for this user. Set wdk_evm_wallet_id explicitly to auto-bind a new network."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return bind_user_evm_wallet(
|
|
203
|
+
user_id,
|
|
204
|
+
wallet_id=next(iter(wallet_ids)),
|
|
205
|
+
network=effective_network,
|
|
206
|
+
service_url=service_url,
|
|
207
|
+
account_index=account_index,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def create_user_evm_wallet(
|
|
212
|
+
user_id: str,
|
|
213
|
+
*,
|
|
214
|
+
password: str,
|
|
215
|
+
label: str | None = None,
|
|
216
|
+
network: str | None = None,
|
|
217
|
+
service_url: str | None = None,
|
|
218
|
+
reveal_seed_phrase: bool = False,
|
|
219
|
+
account_index: int | None = None,
|
|
220
|
+
) -> dict[str, Any]:
|
|
221
|
+
effective_network = _normalize_evm_network(network or settings.solana_network)
|
|
222
|
+
effective_account_index = settings.wdk_evm_account_index if account_index is None else int(account_index)
|
|
223
|
+
client = WdkEvmLocalClient(_resolve_service_url(service_url))
|
|
224
|
+
created = client.post_sync(
|
|
225
|
+
"/v1/evm/wallets/create",
|
|
226
|
+
{
|
|
227
|
+
"label": (label or "").strip() or "Agent EVM Wallet",
|
|
228
|
+
"password": password,
|
|
229
|
+
"network": effective_network,
|
|
230
|
+
"revealSeedPhrase": bool(reveal_seed_phrase),
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
address = client.post_sync(
|
|
234
|
+
"/v1/evm/address/resolve",
|
|
235
|
+
{
|
|
236
|
+
"walletId": created["walletId"],
|
|
237
|
+
"accountIndex": effective_account_index,
|
|
238
|
+
"network": effective_network,
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
binding = {
|
|
242
|
+
"user_id": user_id,
|
|
243
|
+
"wallet_id": str(created["walletId"]),
|
|
244
|
+
"label": str(created.get("label") or "Agent EVM Wallet"),
|
|
245
|
+
"network": effective_network,
|
|
246
|
+
"account_index": effective_account_index,
|
|
247
|
+
"address": str(address.get("address") or ""),
|
|
248
|
+
"storage_format": "local_vault",
|
|
249
|
+
"service_kind": "wdk-evm-wallet",
|
|
250
|
+
"created_at": created.get("createdAt"),
|
|
251
|
+
"updated_at": created.get("updatedAt"),
|
|
252
|
+
}
|
|
253
|
+
_write_wallet_binding(resolve_user_evm_wallet_path(user_id, effective_network), binding)
|
|
254
|
+
return {
|
|
255
|
+
**binding,
|
|
256
|
+
"unlocked": bool(created.get("unlocked", True)),
|
|
257
|
+
"unlock_expires_at": created.get("unlockExpiresAt"),
|
|
258
|
+
**({"seed_phrase": created["seedPhrase"]} if created.get("seedPhrase") else {}),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def import_user_evm_wallet(
|
|
263
|
+
user_id: str,
|
|
264
|
+
*,
|
|
265
|
+
password: str,
|
|
266
|
+
seed_phrase: str,
|
|
267
|
+
label: str | None = None,
|
|
268
|
+
network: str | None = None,
|
|
269
|
+
service_url: str | None = None,
|
|
270
|
+
account_index: int | None = None,
|
|
271
|
+
) -> dict[str, Any]:
|
|
272
|
+
effective_network = _normalize_evm_network(network or settings.solana_network)
|
|
273
|
+
effective_account_index = settings.wdk_evm_account_index if account_index is None else int(account_index)
|
|
274
|
+
client = WdkEvmLocalClient(_resolve_service_url(service_url))
|
|
275
|
+
created = client.post_sync(
|
|
276
|
+
"/v1/evm/wallets/import",
|
|
277
|
+
{
|
|
278
|
+
"label": (label or "").strip() or "Agent EVM Wallet",
|
|
279
|
+
"password": password,
|
|
280
|
+
"seedPhrase": seed_phrase,
|
|
281
|
+
"network": effective_network,
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
address = client.post_sync(
|
|
285
|
+
"/v1/evm/address/resolve",
|
|
286
|
+
{
|
|
287
|
+
"walletId": created["walletId"],
|
|
288
|
+
"accountIndex": effective_account_index,
|
|
289
|
+
"network": effective_network,
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
binding = {
|
|
293
|
+
"user_id": user_id,
|
|
294
|
+
"wallet_id": str(created["walletId"]),
|
|
295
|
+
"label": str(created.get("label") or "Agent EVM Wallet"),
|
|
296
|
+
"network": effective_network,
|
|
297
|
+
"account_index": effective_account_index,
|
|
298
|
+
"address": str(address.get("address") or ""),
|
|
299
|
+
"storage_format": "local_vault",
|
|
300
|
+
"service_kind": "wdk-evm-wallet",
|
|
301
|
+
"created_at": created.get("createdAt"),
|
|
302
|
+
"updated_at": created.get("updatedAt"),
|
|
303
|
+
}
|
|
304
|
+
_write_wallet_binding(resolve_user_evm_wallet_path(user_id, effective_network), binding)
|
|
305
|
+
return {
|
|
306
|
+
**binding,
|
|
307
|
+
"unlocked": bool(created.get("unlocked", True)),
|
|
308
|
+
"unlock_expires_at": created.get("unlockExpiresAt"),
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def unlock_user_evm_wallet(
|
|
313
|
+
user_id: str,
|
|
314
|
+
*,
|
|
315
|
+
password: str,
|
|
316
|
+
network: str | None = None,
|
|
317
|
+
service_url: str | None = None,
|
|
318
|
+
wallet_id: str | None = None,
|
|
319
|
+
account_index: int | None = None,
|
|
320
|
+
) -> dict[str, Any]:
|
|
321
|
+
binding = resolve_user_evm_wallet_binding(
|
|
322
|
+
user_id,
|
|
323
|
+
network=network,
|
|
324
|
+
service_url=service_url,
|
|
325
|
+
wallet_id=wallet_id,
|
|
326
|
+
account_index=account_index,
|
|
327
|
+
)
|
|
328
|
+
client = WdkEvmLocalClient(_resolve_service_url(service_url))
|
|
329
|
+
payload = client.post_sync(
|
|
330
|
+
"/v1/evm/wallets/unlock",
|
|
331
|
+
{
|
|
332
|
+
"walletId": binding["wallet_id"],
|
|
333
|
+
"password": password,
|
|
334
|
+
"timeoutSeconds": 0,
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
return {
|
|
338
|
+
**binding,
|
|
339
|
+
"unlocked": bool(payload.get("unlocked", True)),
|
|
340
|
+
"unlock_expires_at": payload.get("unlockExpiresAt"),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def lock_user_evm_wallet(
|
|
345
|
+
user_id: str,
|
|
346
|
+
*,
|
|
347
|
+
network: str | None = None,
|
|
348
|
+
service_url: str | None = None,
|
|
349
|
+
wallet_id: str | None = None,
|
|
350
|
+
account_index: int | None = None,
|
|
351
|
+
) -> dict[str, Any]:
|
|
352
|
+
binding = resolve_user_evm_wallet_binding(
|
|
353
|
+
user_id,
|
|
354
|
+
network=network,
|
|
355
|
+
service_url=service_url,
|
|
356
|
+
wallet_id=wallet_id,
|
|
357
|
+
account_index=account_index,
|
|
358
|
+
)
|
|
359
|
+
client = WdkEvmLocalClient(_resolve_service_url(service_url))
|
|
360
|
+
payload = client.post_sync(
|
|
361
|
+
"/v1/evm/wallets/lock",
|
|
362
|
+
{
|
|
363
|
+
"walletId": binding["wallet_id"],
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
return {
|
|
367
|
+
**binding,
|
|
368
|
+
"unlocked": bool(payload.get("unlocked", False)),
|
|
369
|
+
"unlock_expires_at": None,
|
|
370
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Filesystem helpers for sensitive wallet/config state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def atomic_write_text(path: Path, content: str, *, mode: int = 0o600) -> None:
|
|
11
|
+
"""Atomically write text to a path with restrictive permissions."""
|
|
12
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
fd, temp_path = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
|
|
14
|
+
try:
|
|
15
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
16
|
+
handle.write(content)
|
|
17
|
+
handle.flush()
|
|
18
|
+
os.fsync(handle.fileno())
|
|
19
|
+
os.chmod(temp_path, mode)
|
|
20
|
+
os.replace(temp_path, path)
|
|
21
|
+
except Exception:
|
|
22
|
+
try:
|
|
23
|
+
os.unlink(temp_path)
|
|
24
|
+
except FileNotFoundError:
|
|
25
|
+
pass
|
|
26
|
+
raise
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def chmod_if_exists(path: Path, mode: int = 0o600) -> None:
|
|
30
|
+
"""Best-effort chmod for sensitive files."""
|
|
31
|
+
try:
|
|
32
|
+
path.chmod(mode)
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
return
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Shared httpx async client for wallet providers."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from agent_wallet.config import settings
|
|
6
|
+
|
|
7
|
+
_client: httpx.AsyncClient | None = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_client() -> httpx.AsyncClient:
|
|
11
|
+
global _client
|
|
12
|
+
if _client is None or _client.is_closed:
|
|
13
|
+
_client = httpx.AsyncClient(
|
|
14
|
+
timeout=httpx.Timeout(settings.http_timeout),
|
|
15
|
+
headers={"Accept": "application/json"},
|
|
16
|
+
follow_redirects=True,
|
|
17
|
+
)
|
|
18
|
+
return _client
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def close_client() -> None:
|
|
22
|
+
global _client
|
|
23
|
+
if _client and not _client.is_closed:
|
|
24
|
+
await _client.aclose()
|
|
25
|
+
_client = None
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Pydantic models for wallet backend state."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentWalletCapabilities(BaseModel):
|
|
9
|
+
backend: str
|
|
10
|
+
chain: str
|
|
11
|
+
custody_model: str
|
|
12
|
+
sign_only: bool
|
|
13
|
+
has_signer: bool
|
|
14
|
+
can_get_address: bool = True
|
|
15
|
+
can_get_balance: bool = True
|
|
16
|
+
can_sign_message: bool = False
|
|
17
|
+
can_sign_transaction: bool = False
|
|
18
|
+
can_send_transaction: bool = False
|
|
19
|
+
external_dependencies: list[str] = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SolanaWalletState(BaseModel):
|
|
23
|
+
chain: str = "solana"
|
|
24
|
+
backend: str
|
|
25
|
+
address: str | None = None
|
|
26
|
+
balance_native: float | None = None
|
|
27
|
+
sign_only: bool = True
|
|
28
|
+
has_signer: bool = False
|
|
29
|
+
source: str = "solana-rpc"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AgentToolSpec(BaseModel):
|
|
33
|
+
name: str
|
|
34
|
+
description: str
|
|
35
|
+
input_schema: dict[str, Any]
|
|
36
|
+
read_only: bool = True
|
|
37
|
+
requires_explicit_user_intent: bool = False
|
|
38
|
+
risk_level: str = "low"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AgentToolResult(BaseModel):
|
|
42
|
+
tool: str
|
|
43
|
+
ok: bool
|
|
44
|
+
data: dict[str, Any] | None = None
|
|
45
|
+
error: str | None = None
|
|
46
|
+
error_code: str | None = None
|
|
47
|
+
error_details: dict[str, Any] | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class OpenClawWalletSessionMetadata(BaseModel):
|
|
51
|
+
user_id: str
|
|
52
|
+
chain: str = "solana"
|
|
53
|
+
network: str
|
|
54
|
+
backend: str
|
|
55
|
+
address: str
|
|
56
|
+
wallet_path: str
|
|
57
|
+
storage_format: str
|
|
58
|
+
created_now: bool
|
|
59
|
+
sign_only: bool
|
|
60
|
+
rpc_provider_mode: str | None = None
|
|
61
|
+
rpc_provider: str | None = None
|
|
62
|
+
rpc_transport: str | None = None
|
|
63
|
+
swap_provider: str | None = None
|
|
64
|
+
swap_transport: str | None = None
|
|
65
|
+
tool_names: list[str]
|
|
66
|
+
approval_token_required_for_execute: bool = True
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""In-memory nonce registry to prevent approval token replay.
|
|
2
|
+
|
|
3
|
+
Each approval token can be used exactly once. After successful verification,
|
|
4
|
+
the token fingerprint is recorded and any subsequent attempt to reuse it is
|
|
5
|
+
rejected with a clear error.
|
|
6
|
+
|
|
7
|
+
For single-process deployments the in-memory registry is sufficient. For
|
|
8
|
+
multi-instance deployments, swap ``_registry`` for a shared store (Redis,
|
|
9
|
+
SQLite, etc.) behind the same interface.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NonceRegistry:
|
|
22
|
+
"""Thread-safe, self-cleaning registry of consumed approval-token fingerprints."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, max_age_seconds: int = 900) -> None:
|
|
25
|
+
self._used: dict[str, float] = {}
|
|
26
|
+
self._lock = threading.Lock()
|
|
27
|
+
self._max_age = max_age_seconds
|
|
28
|
+
|
|
29
|
+
def mark_used(self, token_fingerprint: str) -> None:
|
|
30
|
+
"""Record a token as consumed. Raises on duplicate."""
|
|
31
|
+
with self._lock:
|
|
32
|
+
self._cleanup()
|
|
33
|
+
if token_fingerprint in self._used:
|
|
34
|
+
raise WalletBackendError(
|
|
35
|
+
"Approval token has already been used. "
|
|
36
|
+
"Each approval token is single-use — request a new one."
|
|
37
|
+
)
|
|
38
|
+
self._used[token_fingerprint] = time.monotonic()
|
|
39
|
+
|
|
40
|
+
def _cleanup(self) -> None:
|
|
41
|
+
cutoff = time.monotonic() - self._max_age
|
|
42
|
+
expired = [k for k, t in self._used.items() if t < cutoff]
|
|
43
|
+
for k in expired:
|
|
44
|
+
del self._used[k]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Module-level singleton — shared across the process.
|
|
48
|
+
_registry = NonceRegistry()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def require_single_use(token: str) -> None:
|
|
52
|
+
"""Reject reused approval tokens.
|
|
53
|
+
|
|
54
|
+
Computes a SHA-256 fingerprint of the raw token string and checks it
|
|
55
|
+
against the registry. Safe to call from async or sync contexts because
|
|
56
|
+
the registry uses an ordinary ``threading.Lock``.
|
|
57
|
+
"""
|
|
58
|
+
fp = hashlib.sha256(token.encode("utf-8")).hexdigest()[:16]
|
|
59
|
+
_registry.mark_used(fp)
|