@agentlayer.tech/wallet 0.1.17 → 0.1.19
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 +0 -7
- package/.openclaw/extensions/agent-wallet/README.md +3 -2
- package/.openclaw/extensions/agent-wallet/dist/index.js +105 -7
- package/.openclaw/extensions/agent-wallet/index.ts +105 -7
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +5 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +1 -3
- package/RELEASING.md +5 -15
- package/agent-wallet/README.md +7 -0
- package/agent-wallet/agent_wallet/config.py +11 -0
- package/agent-wallet/agent_wallet/evm_user_wallets.py +310 -2
- package/agent-wallet/agent_wallet/openclaw_adapter.py +303 -1
- package/agent-wallet/agent_wallet/openclaw_runtime.py +10 -41
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +52 -0
- package/agent-wallet/agent_wallet/providers/x402.py +1323 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +30 -0
- package/agent-wallet/pyproject.toml +2 -1
- package/agent-wallet/scripts/build_release_bundle.py +1 -0
- package/agent-wallet/scripts/install_agent_wallet.py +3 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +25 -49
- package/agent-wallet/scripts/install_openclaw_sealed_keys.py +9 -1
- package/package.json +1 -2
- package/wdk-evm-wallet/src/server.js +6 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +108 -0
- package/.openclaw/extensions/pay-bridge/README.md +0 -38
- package/.openclaw/extensions/pay-bridge/core.mjs +0 -287
- package/.openclaw/extensions/pay-bridge/dist/core.mjs +0 -287
- package/.openclaw/extensions/pay-bridge/dist/index.js +0 -196
- package/.openclaw/extensions/pay-bridge/index.ts +0 -196
- package/.openclaw/extensions/pay-bridge/openclaw.plugin.json +0 -34
- package/.openclaw/extensions/pay-bridge/package.json +0 -49
- package/.openclaw/extensions/pay-bridge/skills/pay-operator/SKILL.md +0 -20
- package/.openclaw/extensions/pay-bridge/smoke_pay_bridge.mjs +0 -38
|
@@ -7,7 +7,9 @@ import json
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from agent_wallet.approval import inspect_approval_token, verify_approval_token
|
|
10
|
+
from agent_wallet.exceptions import ProviderError
|
|
10
11
|
from agent_wallet.models import AgentToolResult, AgentToolSpec
|
|
12
|
+
from agent_wallet.providers import x402
|
|
11
13
|
from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendError
|
|
12
14
|
|
|
13
15
|
|
|
@@ -75,7 +77,9 @@ class OpenClawWalletAdapter:
|
|
|
75
77
|
if chain == "bitcoin":
|
|
76
78
|
return normalized == "bitcoin"
|
|
77
79
|
if chain == "evm":
|
|
78
|
-
return normalized in {"ethereum", "base"}
|
|
80
|
+
return normalized in {"ethereum", "base", "eip155:1", "eip155:8453"}
|
|
81
|
+
if chain == "solana":
|
|
82
|
+
return normalized in {"mainnet", "solana:5eykt4usfv8p8njdtrepy1vzkqzkvdp"}
|
|
79
83
|
return normalized == "mainnet"
|
|
80
84
|
|
|
81
85
|
def _is_mainnet(self) -> bool:
|
|
@@ -168,6 +172,115 @@ class OpenClawWalletAdapter:
|
|
|
168
172
|
"Prepare mode requires explicit user intent confirmation."
|
|
169
173
|
)
|
|
170
174
|
|
|
175
|
+
def _x402_tool_specs(self) -> list[AgentToolSpec]:
|
|
176
|
+
return [
|
|
177
|
+
AgentToolSpec(
|
|
178
|
+
name="x402_search_services",
|
|
179
|
+
description=(
|
|
180
|
+
"Search x402-paid services through CDP Bazaar or Agentic Market. "
|
|
181
|
+
"This is read-only discovery and does not spend funds."
|
|
182
|
+
),
|
|
183
|
+
input_schema={
|
|
184
|
+
"type": "object",
|
|
185
|
+
"properties": {
|
|
186
|
+
"query": {"type": "string"},
|
|
187
|
+
"discovery_provider": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"enum": ["auto", "cdp_bazaar", "agentic_market"],
|
|
190
|
+
},
|
|
191
|
+
"network": {"type": "string"},
|
|
192
|
+
"asset": {"type": "string"},
|
|
193
|
+
"scheme": {"type": "string"},
|
|
194
|
+
"max_usd_price": {"type": "string"},
|
|
195
|
+
"limit": {"type": "integer"},
|
|
196
|
+
},
|
|
197
|
+
"additionalProperties": False,
|
|
198
|
+
},
|
|
199
|
+
read_only=True,
|
|
200
|
+
risk_level="low",
|
|
201
|
+
),
|
|
202
|
+
AgentToolSpec(
|
|
203
|
+
name="x402_get_service_details",
|
|
204
|
+
description=(
|
|
205
|
+
"Resolve one x402 service or resource into a normalized details payload. "
|
|
206
|
+
"Use a resource URL for CDP Bazaar or a domain/service id for Agentic Market."
|
|
207
|
+
),
|
|
208
|
+
input_schema={
|
|
209
|
+
"type": "object",
|
|
210
|
+
"properties": {
|
|
211
|
+
"reference": {"type": "string"},
|
|
212
|
+
"discovery_provider": {
|
|
213
|
+
"type": "string",
|
|
214
|
+
"enum": ["auto", "cdp_bazaar", "agentic_market"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
"required": ["reference"],
|
|
218
|
+
"additionalProperties": False,
|
|
219
|
+
},
|
|
220
|
+
read_only=True,
|
|
221
|
+
risk_level="low",
|
|
222
|
+
),
|
|
223
|
+
AgentToolSpec(
|
|
224
|
+
name="x402_preview_request",
|
|
225
|
+
description=(
|
|
226
|
+
"Make an unpaid HTTP request to an x402 endpoint, detect HTTP 402, parse "
|
|
227
|
+
"PAYMENT-REQUIRED, and summarize the payment options. This does not pay or execute."
|
|
228
|
+
),
|
|
229
|
+
input_schema={
|
|
230
|
+
"type": "object",
|
|
231
|
+
"properties": {
|
|
232
|
+
"url": {"type": "string"},
|
|
233
|
+
"method": {"type": "string"},
|
|
234
|
+
"headers": {"type": "object", "additionalProperties": {"type": "string"}},
|
|
235
|
+
"query": {"type": "object", "additionalProperties": True},
|
|
236
|
+
"json_body": {},
|
|
237
|
+
"text_body": {"type": "string"},
|
|
238
|
+
},
|
|
239
|
+
"required": ["url"],
|
|
240
|
+
"additionalProperties": False,
|
|
241
|
+
},
|
|
242
|
+
read_only=True,
|
|
243
|
+
risk_level="low",
|
|
244
|
+
),
|
|
245
|
+
AgentToolSpec(
|
|
246
|
+
name="x402_pay_request",
|
|
247
|
+
description=(
|
|
248
|
+
"Prepare or execute an x402 paid request using the active wallet backend. "
|
|
249
|
+
"This milestone executes the Solana exact buyer flow and keeps EVM as prepare-only."
|
|
250
|
+
),
|
|
251
|
+
input_schema={
|
|
252
|
+
"type": "object",
|
|
253
|
+
"properties": {
|
|
254
|
+
"url": {"type": "string"},
|
|
255
|
+
"method": {"type": "string"},
|
|
256
|
+
"headers": {"type": "object", "additionalProperties": {"type": "string"}},
|
|
257
|
+
"query": {"type": "object", "additionalProperties": True},
|
|
258
|
+
"json_body": {},
|
|
259
|
+
"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
|
+
"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
|
+
},
|
|
275
|
+
"required": ["url", "mode", "purpose"],
|
|
276
|
+
"additionalProperties": False,
|
|
277
|
+
},
|
|
278
|
+
read_only=False,
|
|
279
|
+
requires_explicit_user_intent=True,
|
|
280
|
+
risk_level="high",
|
|
281
|
+
),
|
|
282
|
+
]
|
|
283
|
+
|
|
171
284
|
def _require_execute_approval(
|
|
172
285
|
self,
|
|
173
286
|
*,
|
|
@@ -550,6 +663,23 @@ class OpenClawWalletAdapter:
|
|
|
550
663
|
"btc_transfer_fingerprint": btc_fingerprint,
|
|
551
664
|
}
|
|
552
665
|
|
|
666
|
+
if asset_type == "x402-request":
|
|
667
|
+
return {
|
|
668
|
+
"operation": action_label,
|
|
669
|
+
"network": str(payload.get("network") or getattr(self.backend, "network", "unknown")),
|
|
670
|
+
"wallet": payload.get("wallet"),
|
|
671
|
+
"request_url": payload.get("request_url"),
|
|
672
|
+
"method": payload.get("method"),
|
|
673
|
+
"request_fingerprint": payload.get("request_fingerprint"),
|
|
674
|
+
"body_hash": payload.get("body_hash"),
|
|
675
|
+
"x402_network": payload.get("x402_network"),
|
|
676
|
+
"x402_scheme": payload.get("x402_scheme"),
|
|
677
|
+
"x402_asset": payload.get("x402_asset"),
|
|
678
|
+
"x402_amount": payload.get("x402_amount"),
|
|
679
|
+
"x402_amount_display": payload.get("x402_amount_display"),
|
|
680
|
+
"x402_pay_to": payload.get("x402_pay_to"),
|
|
681
|
+
}
|
|
682
|
+
|
|
553
683
|
if asset_type == "evm-native-transfer":
|
|
554
684
|
evm_binding = {
|
|
555
685
|
"recipient": payload.get("recipient"),
|
|
@@ -3038,6 +3168,7 @@ class OpenClawWalletAdapter:
|
|
|
3038
3168
|
)
|
|
3039
3169
|
)
|
|
3040
3170
|
|
|
3171
|
+
tools.extend(self._x402_tool_specs())
|
|
3041
3172
|
return [tool for tool in tools if tool.name not in TEMPORARILY_DISABLED_TOOLS]
|
|
3042
3173
|
|
|
3043
3174
|
def get_runtime_instructions(self) -> str:
|
|
@@ -3054,6 +3185,169 @@ class OpenClawWalletAdapter:
|
|
|
3054
3185
|
f"{tool_name} is temporarily disabled. The implementation remains in the repo but this tool is currently turned off."
|
|
3055
3186
|
)
|
|
3056
3187
|
|
|
3188
|
+
if tool_name == "x402_search_services":
|
|
3189
|
+
query = args.get("query")
|
|
3190
|
+
discovery_provider = args.get("discovery_provider", "auto")
|
|
3191
|
+
network = args.get("network")
|
|
3192
|
+
asset = args.get("asset")
|
|
3193
|
+
scheme = args.get("scheme")
|
|
3194
|
+
max_usd_price = args.get("max_usd_price")
|
|
3195
|
+
limit = args.get("limit", 10)
|
|
3196
|
+
for field_name, value in (
|
|
3197
|
+
("query", query),
|
|
3198
|
+
("discovery_provider", discovery_provider),
|
|
3199
|
+
("network", network),
|
|
3200
|
+
("asset", asset),
|
|
3201
|
+
("scheme", scheme),
|
|
3202
|
+
("max_usd_price", max_usd_price),
|
|
3203
|
+
):
|
|
3204
|
+
if value is not None and not isinstance(value, str):
|
|
3205
|
+
raise WalletBackendError(f"{field_name} must be a string when provided.")
|
|
3206
|
+
if not isinstance(limit, int) or limit <= 0:
|
|
3207
|
+
raise WalletBackendError("limit must be a positive integer.")
|
|
3208
|
+
data = await x402.search_services(
|
|
3209
|
+
query=query,
|
|
3210
|
+
discovery_provider=discovery_provider,
|
|
3211
|
+
network=network,
|
|
3212
|
+
asset=asset,
|
|
3213
|
+
scheme=scheme,
|
|
3214
|
+
max_usd_price=max_usd_price,
|
|
3215
|
+
limit=limit,
|
|
3216
|
+
)
|
|
3217
|
+
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
3218
|
+
|
|
3219
|
+
if tool_name == "x402_get_service_details":
|
|
3220
|
+
reference = args.get("reference")
|
|
3221
|
+
discovery_provider = args.get("discovery_provider", "auto")
|
|
3222
|
+
if not isinstance(reference, str) or not reference.strip():
|
|
3223
|
+
raise WalletBackendError("reference is required.")
|
|
3224
|
+
if discovery_provider is not None and not isinstance(discovery_provider, str):
|
|
3225
|
+
raise WalletBackendError("discovery_provider must be a string when provided.")
|
|
3226
|
+
data = await x402.get_service_details(
|
|
3227
|
+
reference=reference.strip(),
|
|
3228
|
+
discovery_provider=discovery_provider,
|
|
3229
|
+
)
|
|
3230
|
+
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
3231
|
+
|
|
3232
|
+
if tool_name == "x402_preview_request":
|
|
3233
|
+
url = args.get("url")
|
|
3234
|
+
method = args.get("method", "GET")
|
|
3235
|
+
headers = args.get("headers")
|
|
3236
|
+
query = args.get("query")
|
|
3237
|
+
json_body = args.get("json_body")
|
|
3238
|
+
text_body = args.get("text_body")
|
|
3239
|
+
if not isinstance(url, str) or not url.strip():
|
|
3240
|
+
raise WalletBackendError("url is required.")
|
|
3241
|
+
if method is not None and not isinstance(method, str):
|
|
3242
|
+
raise WalletBackendError("method must be a string when provided.")
|
|
3243
|
+
if headers is not None and not isinstance(headers, dict):
|
|
3244
|
+
raise WalletBackendError("headers must be an object when provided.")
|
|
3245
|
+
if query is not None and not isinstance(query, dict):
|
|
3246
|
+
raise WalletBackendError("query must be an object when provided.")
|
|
3247
|
+
if text_body is not None and not isinstance(text_body, str):
|
|
3248
|
+
raise WalletBackendError("text_body must be a string when provided.")
|
|
3249
|
+
data = await x402.preview_request(
|
|
3250
|
+
backend=active_backend,
|
|
3251
|
+
url=url.strip(),
|
|
3252
|
+
method=method,
|
|
3253
|
+
headers=headers,
|
|
3254
|
+
query=query,
|
|
3255
|
+
json_body=json_body,
|
|
3256
|
+
text_body=text_body,
|
|
3257
|
+
)
|
|
3258
|
+
if data.get("payment_required"):
|
|
3259
|
+
data = self._annotate_sensitive_payload(
|
|
3260
|
+
data,
|
|
3261
|
+
action_label="x402 paid request",
|
|
3262
|
+
mode="preview",
|
|
3263
|
+
)
|
|
3264
|
+
approval_hint = dict(data.get("approval_hint") or {})
|
|
3265
|
+
approval_hint["tool_name"] = "x402_pay_request"
|
|
3266
|
+
data["approval_hint"] = approval_hint
|
|
3267
|
+
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
3268
|
+
|
|
3269
|
+
if tool_name == "x402_pay_request":
|
|
3270
|
+
url = args.get("url")
|
|
3271
|
+
method = args.get("method", "GET")
|
|
3272
|
+
headers = args.get("headers")
|
|
3273
|
+
query = args.get("query")
|
|
3274
|
+
json_body = args.get("json_body")
|
|
3275
|
+
text_body = args.get("text_body")
|
|
3276
|
+
mode = str(args.get("mode") or "").strip().lower()
|
|
3277
|
+
purpose = args.get("purpose")
|
|
3278
|
+
user_intent = args.get("user_intent")
|
|
3279
|
+
approval_token = args.get("approval_token")
|
|
3280
|
+
if not isinstance(url, str) or not url.strip():
|
|
3281
|
+
raise WalletBackendError("url is required.")
|
|
3282
|
+
if method is not None and not isinstance(method, str):
|
|
3283
|
+
raise WalletBackendError("method must be a string when provided.")
|
|
3284
|
+
if headers is not None and not isinstance(headers, dict):
|
|
3285
|
+
raise WalletBackendError("headers must be an object when provided.")
|
|
3286
|
+
if query is not None and not isinstance(query, dict):
|
|
3287
|
+
raise WalletBackendError("query must be an object when provided.")
|
|
3288
|
+
if text_body is not None and not isinstance(text_body, str):
|
|
3289
|
+
raise WalletBackendError("text_body must be a string when provided.")
|
|
3290
|
+
if mode not in {"prepare", "execute"}:
|
|
3291
|
+
raise WalletBackendError("mode must be 'prepare' or 'execute'.")
|
|
3292
|
+
if not isinstance(purpose, str) or not purpose.strip():
|
|
3293
|
+
raise WalletBackendError("purpose is required.")
|
|
3294
|
+
if mode == "prepare":
|
|
3295
|
+
self._require_prepare_intent(user_intent)
|
|
3296
|
+
data = await x402.prepare_request(
|
|
3297
|
+
backend=active_backend,
|
|
3298
|
+
url=url.strip(),
|
|
3299
|
+
method=method,
|
|
3300
|
+
headers=headers,
|
|
3301
|
+
query=query,
|
|
3302
|
+
json_body=json_body,
|
|
3303
|
+
text_body=text_body,
|
|
3304
|
+
)
|
|
3305
|
+
data["purpose"] = purpose.strip()
|
|
3306
|
+
data = self._annotate_sensitive_payload(
|
|
3307
|
+
data,
|
|
3308
|
+
action_label="x402 paid request",
|
|
3309
|
+
mode="prepare",
|
|
3310
|
+
)
|
|
3311
|
+
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
3312
|
+
preview = await x402.prepare_request(
|
|
3313
|
+
backend=active_backend,
|
|
3314
|
+
url=url.strip(),
|
|
3315
|
+
method=method,
|
|
3316
|
+
headers=headers,
|
|
3317
|
+
query=query,
|
|
3318
|
+
json_body=json_body,
|
|
3319
|
+
text_body=text_body,
|
|
3320
|
+
)
|
|
3321
|
+
preview["purpose"] = purpose.strip()
|
|
3322
|
+
preview = self._annotate_sensitive_payload(
|
|
3323
|
+
preview,
|
|
3324
|
+
action_label="x402 paid request",
|
|
3325
|
+
mode="execute",
|
|
3326
|
+
)
|
|
3327
|
+
self._require_execute_approval(
|
|
3328
|
+
approval_token=approval_token,
|
|
3329
|
+
tool_name=tool_name,
|
|
3330
|
+
summary=preview["confirmation_summary"],
|
|
3331
|
+
action_label="x402 paid request",
|
|
3332
|
+
backend=active_backend,
|
|
3333
|
+
)
|
|
3334
|
+
data = await x402.execute_request(
|
|
3335
|
+
backend=active_backend,
|
|
3336
|
+
url=url.strip(),
|
|
3337
|
+
method=method,
|
|
3338
|
+
headers=headers,
|
|
3339
|
+
query=query,
|
|
3340
|
+
json_body=json_body,
|
|
3341
|
+
text_body=text_body,
|
|
3342
|
+
)
|
|
3343
|
+
data["purpose"] = purpose.strip()
|
|
3344
|
+
data = self._annotate_sensitive_payload(
|
|
3345
|
+
data,
|
|
3346
|
+
action_label="x402 paid request",
|
|
3347
|
+
mode="execute",
|
|
3348
|
+
)
|
|
3349
|
+
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
3350
|
+
|
|
3057
3351
|
if tool_name == "get_wallet_capabilities":
|
|
3058
3352
|
data = active_backend.get_capabilities().to_dict()
|
|
3059
3353
|
data["network"] = str(getattr(active_backend, "network", "unknown"))
|
|
@@ -6053,4 +6347,12 @@ class OpenClawWalletAdapter:
|
|
|
6053
6347
|
error_code=exc.code,
|
|
6054
6348
|
error_details=exc.details,
|
|
6055
6349
|
)
|
|
6350
|
+
if isinstance(exc, ProviderError):
|
|
6351
|
+
return AgentToolResult(
|
|
6352
|
+
tool=tool_name,
|
|
6353
|
+
ok=False,
|
|
6354
|
+
error=str(exc),
|
|
6355
|
+
error_code=exc.provider,
|
|
6356
|
+
error_details=exc.details,
|
|
6357
|
+
)
|
|
6056
6358
|
return AgentToolResult(tool=tool_name, ok=False, error=str(exc))
|
|
@@ -8,7 +8,7 @@ from typing import Any
|
|
|
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
10
|
from agent_wallet.config import settings
|
|
11
|
-
from agent_wallet.evm_user_wallets import
|
|
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
|
|
14
14
|
from agent_wallet.plugin_bundle import build_openclaw_plugin_bundle
|
|
@@ -173,38 +173,17 @@ def onboard_openclaw_user_wallet(
|
|
|
173
173
|
"base_sepolia": "base-sepolia",
|
|
174
174
|
}
|
|
175
175
|
effective_network = aliases.get(requested_network, requested_network)
|
|
176
|
-
binding: dict[str, Any] | None = None
|
|
177
176
|
wallet_id = str(wdk_evm_wallet_id or settings.wdk_evm_wallet_id).strip()
|
|
178
177
|
if not service_url:
|
|
179
178
|
raise WalletBackendError("wdk_evm_service_url is required for backend=wdk_evm_local.")
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
wallet_id=wallet_id,
|
|
189
|
-
account_index=account_index,
|
|
190
|
-
)
|
|
191
|
-
else:
|
|
192
|
-
if str(binding.get("wallet_id") or "").strip() != wallet_id:
|
|
193
|
-
binding = ensure_user_evm_wallet_binding(
|
|
194
|
-
user_id,
|
|
195
|
-
network=effective_network,
|
|
196
|
-
service_url=service_url,
|
|
197
|
-
wallet_id=wallet_id,
|
|
198
|
-
account_index=account_index,
|
|
199
|
-
)
|
|
200
|
-
else:
|
|
201
|
-
binding = ensure_user_evm_wallet_binding(
|
|
202
|
-
user_id,
|
|
203
|
-
network=effective_network,
|
|
204
|
-
service_url=service_url,
|
|
205
|
-
account_index=account_index,
|
|
206
|
-
)
|
|
207
|
-
wallet_id = str(binding.get("wallet_id") or "").strip()
|
|
179
|
+
binding = ensure_user_evm_wallet_ready(
|
|
180
|
+
user_id,
|
|
181
|
+
network=effective_network,
|
|
182
|
+
service_url=service_url,
|
|
183
|
+
wallet_id=wallet_id or None,
|
|
184
|
+
account_index=account_index,
|
|
185
|
+
)
|
|
186
|
+
wallet_id = str(binding.get("wallet_id") or wallet_id).strip()
|
|
208
187
|
if not wallet_id:
|
|
209
188
|
raise WalletBackendError(
|
|
210
189
|
"wdk_evm_wallet_id is required for backend=wdk_evm_local, or create a bound user EVM wallet first."
|
|
@@ -212,17 +191,7 @@ def onboard_openclaw_user_wallet(
|
|
|
212
191
|
|
|
213
192
|
client = WdkEvmLocalClient(service_url)
|
|
214
193
|
wallet_meta = client.post_sync("/v1/evm/wallets/get", {"walletId": wallet_id})
|
|
215
|
-
resolved_address = str(
|
|
216
|
-
if not resolved_address:
|
|
217
|
-
address_payload = client.post_sync(
|
|
218
|
-
"/v1/evm/address/resolve",
|
|
219
|
-
{
|
|
220
|
-
"walletId": wallet_id,
|
|
221
|
-
"accountIndex": account_index,
|
|
222
|
-
"network": effective_network,
|
|
223
|
-
},
|
|
224
|
-
)
|
|
225
|
-
resolved_address = str(address_payload.get("address") or "").strip()
|
|
194
|
+
resolved_address = str(binding.get("address") or "").strip()
|
|
226
195
|
backend = WdkEvmLocalWalletBackend(
|
|
227
196
|
service_url=service_url,
|
|
228
197
|
wallet_id=wallet_id,
|
|
@@ -102,6 +102,35 @@ def _unwrap_payload(response: httpx.Response) -> dict[str, Any]:
|
|
|
102
102
|
return data
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def _unwrap_list_payload(response: httpx.Response) -> list[dict[str, Any]]:
|
|
106
|
+
try:
|
|
107
|
+
payload = response.json()
|
|
108
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
109
|
+
raise WalletBackendError(
|
|
110
|
+
f"wdk-evm-wallet returned a non-JSON response ({response.status_code}).",
|
|
111
|
+
code="network_unavailable",
|
|
112
|
+
details={
|
|
113
|
+
"service": "wdk-evm-wallet",
|
|
114
|
+
"http_status": response.status_code,
|
|
115
|
+
},
|
|
116
|
+
) from exc
|
|
117
|
+
if response.status_code >= 400 or payload.get("ok") is False:
|
|
118
|
+
detail = payload.get("error") or f"HTTP {response.status_code}"
|
|
119
|
+
raise WalletBackendError(
|
|
120
|
+
str(detail),
|
|
121
|
+
code=str(payload.get("error_code") or "").strip() or None,
|
|
122
|
+
details=_error_details_from_payload(payload),
|
|
123
|
+
)
|
|
124
|
+
data = payload.get("data")
|
|
125
|
+
if not isinstance(data, list):
|
|
126
|
+
raise WalletBackendError("wdk-evm-wallet returned an invalid list response payload.")
|
|
127
|
+
wallets: list[dict[str, Any]] = []
|
|
128
|
+
for item in data:
|
|
129
|
+
if isinstance(item, dict):
|
|
130
|
+
wallets.append(dict(item))
|
|
131
|
+
return wallets
|
|
132
|
+
|
|
133
|
+
|
|
105
134
|
class WdkEvmLocalClient:
|
|
106
135
|
"""Small client for the local EVM wallet service."""
|
|
107
136
|
|
|
@@ -203,3 +232,26 @@ class WdkEvmLocalClient:
|
|
|
203
232
|
details={"service": "wdk-evm-wallet", "path": path},
|
|
204
233
|
) from exc
|
|
205
234
|
return _unwrap_payload(response)
|
|
235
|
+
|
|
236
|
+
def list_wallets_sync(self) -> list[dict[str, Any]]:
|
|
237
|
+
try:
|
|
238
|
+
with httpx.Client(
|
|
239
|
+
timeout=float(settings.http_timeout),
|
|
240
|
+
headers=self._headers,
|
|
241
|
+
follow_redirects=False,
|
|
242
|
+
trust_env=False,
|
|
243
|
+
) as client:
|
|
244
|
+
response = client.get(f"{self.base_url}/v1/evm/wallets")
|
|
245
|
+
except httpx.TimeoutException as exc:
|
|
246
|
+
raise WalletBackendError(
|
|
247
|
+
"wdk-evm-wallet request timed out.",
|
|
248
|
+
code="network_unavailable",
|
|
249
|
+
details={"service": "wdk-evm-wallet", "path": "/v1/evm/wallets"},
|
|
250
|
+
) from exc
|
|
251
|
+
except httpx.RequestError as exc:
|
|
252
|
+
raise WalletBackendError(
|
|
253
|
+
f"wdk-evm-wallet request failed: {exc}",
|
|
254
|
+
code="network_unavailable",
|
|
255
|
+
details={"service": "wdk-evm-wallet", "path": "/v1/evm/wallets"},
|
|
256
|
+
) from exc
|
|
257
|
+
return _unwrap_list_payload(response)
|