@agentlayer.tech/wallet 0.1.27 → 0.1.28
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/dist/index.js +2 -11
- package/.openclaw/extensions/agent-wallet/index.ts +2 -11
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +14 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +44 -76
- package/agent-wallet/agent_wallet/providers/x402.py +194 -9
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +11 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/package.json +1 -1
|
@@ -956,7 +956,7 @@ const walletSessionToolDefinitions = [
|
|
|
956
956
|
{
|
|
957
957
|
name: "x402_pay_request",
|
|
958
958
|
description:
|
|
959
|
-
|
|
959
|
+
"Pay for and call an x402 endpoint using the active wallet backend. The tool probes the endpoint, validates compatibility, signs the payment, and returns the service response in one call.",
|
|
960
960
|
parameters: {
|
|
961
961
|
type: "object",
|
|
962
962
|
properties: {
|
|
@@ -966,18 +966,9 @@ const walletSessionToolDefinitions = [
|
|
|
966
966
|
query: { type: "object", additionalProperties: true },
|
|
967
967
|
json_body: {},
|
|
968
968
|
text_body: { type: "string" },
|
|
969
|
-
mode: {
|
|
970
|
-
type: "string",
|
|
971
|
-
enum: ["prepare", "execute"],
|
|
972
|
-
description: "prepare validates the payment plan; execute sends the paid retry.",
|
|
973
|
-
},
|
|
974
969
|
purpose: { type: "string" },
|
|
975
|
-
user_intent: {
|
|
976
|
-
type: "boolean",
|
|
977
|
-
description: "Must be true for prepare mode.",
|
|
978
|
-
},
|
|
979
970
|
},
|
|
980
|
-
required: ["url", "
|
|
971
|
+
required: ["url", "purpose"],
|
|
981
972
|
additionalProperties: false,
|
|
982
973
|
},
|
|
983
974
|
},
|
|
@@ -956,7 +956,7 @@ const walletSessionToolDefinitions = [
|
|
|
956
956
|
{
|
|
957
957
|
name: "x402_pay_request",
|
|
958
958
|
description:
|
|
959
|
-
|
|
959
|
+
"Pay for and call an x402 endpoint using the active wallet backend. The tool probes the endpoint, validates compatibility, signs the payment, and returns the service response in one call.",
|
|
960
960
|
parameters: {
|
|
961
961
|
type: "object",
|
|
962
962
|
properties: {
|
|
@@ -966,18 +966,9 @@ const walletSessionToolDefinitions = [
|
|
|
966
966
|
query: { type: "object", additionalProperties: true },
|
|
967
967
|
json_body: {},
|
|
968
968
|
text_body: { type: "string" },
|
|
969
|
-
mode: {
|
|
970
|
-
type: "string",
|
|
971
|
-
enum: ["prepare", "execute"],
|
|
972
|
-
description: "prepare validates the payment plan; execute sends the paid retry.",
|
|
973
|
-
},
|
|
974
969
|
purpose: { type: "string" },
|
|
975
|
-
user_intent: {
|
|
976
|
-
type: "boolean",
|
|
977
|
-
description: "Must be true for prepare mode.",
|
|
978
|
-
},
|
|
979
970
|
},
|
|
980
|
-
required: ["url", "
|
|
971
|
+
required: ["url", "purpose"],
|
|
981
972
|
additionalProperties: false,
|
|
982
973
|
},
|
|
983
974
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.1.28 - 2026-05-28
|
|
6
|
+
|
|
7
|
+
- Simplified `x402_pay_request` into a single-shot paid execution flow while
|
|
8
|
+
keeping `x402_preview_request` as an optional research tool.
|
|
9
|
+
- Hardened x402 execution with explicit payment requirement validation, longer
|
|
10
|
+
paid-request timeouts, structured settlement logging, and safer settlement
|
|
11
|
+
header parsing.
|
|
12
|
+
- Fixed Base x402 EVM signing by normalizing typed-data byte fields before
|
|
13
|
+
sending them through the local WDK signer bridge.
|
|
14
|
+
- Reduced x402 preview confusion in OpenClaw by exposing a payment summary
|
|
15
|
+
without approval-token style confirmation semantics.
|
|
16
|
+
- Added an early safety guard for `x402.alchemy.com` so unsupported wallet-auth
|
|
17
|
+
flows fail before spending funds.
|
|
18
|
+
|
|
5
19
|
## v0.1.27 - 2026-05-27
|
|
6
20
|
|
|
7
21
|
- Improved Solana swap fallback landing by enabling Jupiter dynamic slippage
|
|
@@ -245,8 +245,9 @@ class OpenClawWalletAdapter:
|
|
|
245
245
|
AgentToolSpec(
|
|
246
246
|
name="x402_pay_request",
|
|
247
247
|
description=(
|
|
248
|
-
"
|
|
249
|
-
"
|
|
248
|
+
"Pay for and call an x402 endpoint using the active wallet backend. "
|
|
249
|
+
"The tool probes the endpoint, validates compatibility, signs the payment, "
|
|
250
|
+
"and returns the service response in one call."
|
|
250
251
|
),
|
|
251
252
|
input_schema={
|
|
252
253
|
"type": "object",
|
|
@@ -257,26 +258,13 @@ class OpenClawWalletAdapter:
|
|
|
257
258
|
"query": {"type": "object", "additionalProperties": True},
|
|
258
259
|
"json_body": {},
|
|
259
260
|
"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
261
|
"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
262
|
},
|
|
275
|
-
"required": ["url", "
|
|
263
|
+
"required": ["url", "purpose"],
|
|
276
264
|
"additionalProperties": False,
|
|
277
265
|
},
|
|
278
266
|
read_only=False,
|
|
279
|
-
requires_explicit_user_intent=
|
|
267
|
+
requires_explicit_user_intent=False,
|
|
280
268
|
risk_level="high",
|
|
281
269
|
),
|
|
282
270
|
]
|
|
@@ -872,6 +860,42 @@ class OpenClawWalletAdapter:
|
|
|
872
860
|
)
|
|
873
861
|
return annotated
|
|
874
862
|
|
|
863
|
+
def _annotate_x402_payload(
|
|
864
|
+
self,
|
|
865
|
+
payload: dict[str, Any],
|
|
866
|
+
*,
|
|
867
|
+
mode: str,
|
|
868
|
+
) -> dict[str, Any]:
|
|
869
|
+
if not payload.get("payment_required"):
|
|
870
|
+
return dict(payload)
|
|
871
|
+
annotated = self._annotate_sensitive_payload(
|
|
872
|
+
payload,
|
|
873
|
+
action_label="x402 paid request",
|
|
874
|
+
mode=mode,
|
|
875
|
+
)
|
|
876
|
+
summary = dict(annotated.get("confirmation_summary") or {})
|
|
877
|
+
if summary:
|
|
878
|
+
annotated["payment_summary"] = summary
|
|
879
|
+
requirements = dict(annotated.get("confirmation_requirements") or {})
|
|
880
|
+
requirements["prepare_requires_user_intent"] = False
|
|
881
|
+
requirements["execute_requires_approval_token"] = False
|
|
882
|
+
requirements["execute_requires_mainnet_confirmed_in_token"] = False
|
|
883
|
+
annotated.pop("approval_hint", None)
|
|
884
|
+
if mode == "preview":
|
|
885
|
+
annotated.pop("confirmation_summary", None)
|
|
886
|
+
annotated.pop("confirmation_requirements", None)
|
|
887
|
+
if annotated.get("is_mainnet"):
|
|
888
|
+
annotated["preview_note"] = (
|
|
889
|
+
"This is a paid mainnet endpoint preview only. Review the service URL, network, asset, amount, and payment destination before calling x402_pay_request."
|
|
890
|
+
)
|
|
891
|
+
return annotated
|
|
892
|
+
annotated["confirmation_requirements"] = requirements
|
|
893
|
+
if annotated.get("is_mainnet"):
|
|
894
|
+
annotated["mainnet_warning"] = (
|
|
895
|
+
"Mainnet x402 payment. Confirm the service URL, network, asset, amount, and payment destination before paying."
|
|
896
|
+
)
|
|
897
|
+
return annotated
|
|
898
|
+
|
|
875
899
|
def list_tools(self) -> list[AgentToolSpec]:
|
|
876
900
|
"""Return wallet tools suitable for agent registration."""
|
|
877
901
|
capabilities = self.backend.get_capabilities()
|
|
@@ -3309,14 +3333,7 @@ class OpenClawWalletAdapter:
|
|
|
3309
3333
|
text_body=text_body,
|
|
3310
3334
|
)
|
|
3311
3335
|
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
|
|
3336
|
+
data = self._annotate_x402_payload(data, mode="preview")
|
|
3320
3337
|
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
3321
3338
|
|
|
3322
3339
|
if tool_name == "x402_pay_request":
|
|
@@ -3326,10 +3343,7 @@ class OpenClawWalletAdapter:
|
|
|
3326
3343
|
query = args.get("query")
|
|
3327
3344
|
json_body = args.get("json_body")
|
|
3328
3345
|
text_body = args.get("text_body")
|
|
3329
|
-
mode = str(args.get("mode") or "").strip().lower()
|
|
3330
3346
|
purpose = args.get("purpose")
|
|
3331
|
-
user_intent = args.get("user_intent")
|
|
3332
|
-
approval_token = args.get("approval_token")
|
|
3333
3347
|
if not isinstance(url, str) or not url.strip():
|
|
3334
3348
|
raise WalletBackendError("url is required.")
|
|
3335
3349
|
if method is not None and not isinstance(method, str):
|
|
@@ -3340,51 +3354,9 @@ class OpenClawWalletAdapter:
|
|
|
3340
3354
|
raise WalletBackendError("query must be an object when provided.")
|
|
3341
3355
|
if text_body is not None and not isinstance(text_body, str):
|
|
3342
3356
|
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
3357
|
if not isinstance(purpose, str) or not purpose.strip():
|
|
3346
3358
|
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(
|
|
3359
|
+
data = await x402.pay_and_fetch(
|
|
3388
3360
|
backend=active_backend,
|
|
3389
3361
|
url=url.strip(),
|
|
3390
3362
|
method=method,
|
|
@@ -3394,11 +3366,7 @@ class OpenClawWalletAdapter:
|
|
|
3394
3366
|
text_body=text_body,
|
|
3395
3367
|
)
|
|
3396
3368
|
data["purpose"] = purpose.strip()
|
|
3397
|
-
data = self.
|
|
3398
|
-
data,
|
|
3399
|
-
action_label="x402 paid request",
|
|
3400
|
-
mode="execute",
|
|
3401
|
-
)
|
|
3369
|
+
data = self._annotate_x402_payload(data, mode="execute")
|
|
3402
3370
|
return AgentToolResult(tool=tool_name, ok=True, data=data)
|
|
3403
3371
|
|
|
3404
3372
|
if tool_name == "get_wallet_capabilities":
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import base64
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
|
+
import logging
|
|
8
9
|
from typing import Any
|
|
9
10
|
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
10
11
|
|
|
@@ -15,6 +16,7 @@ from agent_wallet.wallet_layer.base import AgentWalletBackend
|
|
|
15
16
|
|
|
16
17
|
CDP_BAZAAR_DISCOVERY_BASE_URL = "https://api.cdp.coinbase.com/platform/v2/x402/discovery"
|
|
17
18
|
AGENTIC_MARKET_API_BASE_URL = "https://api.agentic.market/v1"
|
|
19
|
+
X402_EXECUTE_TIMEOUT_SECONDS = 45.0
|
|
18
20
|
SOLANA_CAIP_BY_NETWORK = {
|
|
19
21
|
"mainnet": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
20
22
|
"devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
@@ -32,6 +34,7 @@ _USDC_IDENTIFIERS = {
|
|
|
32
34
|
"0x036cbd53842c5426634e7929541ec2318f3dcf7e",
|
|
33
35
|
"epjfwdd5aufqssqem2qn1xzybapc8g4wegkgkzwytdt1v",
|
|
34
36
|
}
|
|
37
|
+
log = logging.getLogger("agent_wallet.x402")
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
def _backend_chain(backend: AgentWalletBackend) -> str:
|
|
@@ -137,6 +140,13 @@ def _append_query(url: str, query: dict[str, str]) -> str:
|
|
|
137
140
|
)
|
|
138
141
|
|
|
139
142
|
|
|
143
|
+
def _request_host(url: str) -> str:
|
|
144
|
+
try:
|
|
145
|
+
return _trim(urlsplit(url).netloc).lower()
|
|
146
|
+
except Exception:
|
|
147
|
+
return ""
|
|
148
|
+
|
|
149
|
+
|
|
140
150
|
def _response_text(response: Any) -> str:
|
|
141
151
|
try:
|
|
142
152
|
text = response.text
|
|
@@ -513,6 +523,7 @@ def _build_request_metadata(
|
|
|
513
523
|
)
|
|
514
524
|
return {
|
|
515
525
|
"url": final_url,
|
|
526
|
+
"host": _request_host(final_url),
|
|
516
527
|
"method": http_method,
|
|
517
528
|
"headers": normalized_headers,
|
|
518
529
|
"query": normalized_query,
|
|
@@ -529,6 +540,7 @@ async def _send_request(
|
|
|
529
540
|
client: Any,
|
|
530
541
|
request: dict[str, Any],
|
|
531
542
|
extra_headers: dict[str, str] | None = None,
|
|
543
|
+
timeout: float | None = None,
|
|
532
544
|
) -> Any:
|
|
533
545
|
headers = dict(request["headers"])
|
|
534
546
|
if extra_headers:
|
|
@@ -539,6 +551,7 @@ async def _send_request(
|
|
|
539
551
|
headers=headers,
|
|
540
552
|
json=request["json_body"] if request["json_body"] is not None else None,
|
|
541
553
|
content=request["text_body"] if request["text_body"] is not None else None,
|
|
554
|
+
timeout=timeout,
|
|
542
555
|
)
|
|
543
556
|
|
|
544
557
|
|
|
@@ -671,6 +684,96 @@ def _require_executable_payment(
|
|
|
671
684
|
return selected
|
|
672
685
|
|
|
673
686
|
|
|
687
|
+
def _validate_payment_requirement(
|
|
688
|
+
selected: dict[str, Any] | None,
|
|
689
|
+
*,
|
|
690
|
+
backend: AgentWalletBackend,
|
|
691
|
+
request_url: str,
|
|
692
|
+
) -> dict[str, Any]:
|
|
693
|
+
if not isinstance(selected, dict):
|
|
694
|
+
raise ProviderError(
|
|
695
|
+
"x402-validate",
|
|
696
|
+
"This endpoint returned HTTP 402 but no compatible payment option was found for the active wallet.",
|
|
697
|
+
details={
|
|
698
|
+
"request_url": request_url,
|
|
699
|
+
"wallet_chain": _backend_chain(backend),
|
|
700
|
+
"wallet_network": _backend_network(backend),
|
|
701
|
+
},
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
scheme = _trim(selected.get("scheme")).lower()
|
|
705
|
+
if scheme != "exact":
|
|
706
|
+
raise ProviderError(
|
|
707
|
+
"x402-validate",
|
|
708
|
+
f"Unsupported x402 payment scheme '{scheme or 'unknown'}'. Only 'exact' is supported.",
|
|
709
|
+
details={"request_url": request_url, "selected_payment": selected},
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
if not _trim(selected.get("pay_to")):
|
|
713
|
+
raise ProviderError(
|
|
714
|
+
"x402-validate",
|
|
715
|
+
"Payment destination (payTo) is missing from the x402 requirement.",
|
|
716
|
+
details={"request_url": request_url, "selected_payment": selected},
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
compatibility = _requirement_compatibility(selected, backend)
|
|
720
|
+
if compatibility["currently_executable"]:
|
|
721
|
+
return selected
|
|
722
|
+
|
|
723
|
+
chain = _backend_chain(backend) or "unknown"
|
|
724
|
+
network = _backend_network(backend) or "unknown"
|
|
725
|
+
requirement_network = _trim(selected.get("network")) or "unknown"
|
|
726
|
+
if chain == "solana" and requirement_network not in SOLANA_CAIP_BY_NETWORK.values():
|
|
727
|
+
message = (
|
|
728
|
+
f"This endpoint requires payment on {requirement_network}, but the active wallet is Solana ({network})."
|
|
729
|
+
)
|
|
730
|
+
elif chain == "evm" and requirement_network not in EVM_CAIP_BY_NETWORK.values():
|
|
731
|
+
message = (
|
|
732
|
+
f"This endpoint requires payment on {requirement_network}, but the active wallet is EVM ({network})."
|
|
733
|
+
)
|
|
734
|
+
else:
|
|
735
|
+
message = str(compatibility["reason"])
|
|
736
|
+
|
|
737
|
+
raise ProviderError(
|
|
738
|
+
"x402-validate",
|
|
739
|
+
message,
|
|
740
|
+
details={
|
|
741
|
+
"request_url": request_url,
|
|
742
|
+
"selected_payment": selected,
|
|
743
|
+
"compatibility": compatibility,
|
|
744
|
+
},
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _validate_request_execution_policy(
|
|
749
|
+
*,
|
|
750
|
+
request: dict[str, Any],
|
|
751
|
+
backend: AgentWalletBackend,
|
|
752
|
+
) -> None:
|
|
753
|
+
host = _trim(request.get("host")).lower()
|
|
754
|
+
if host == "x402.alchemy.com":
|
|
755
|
+
headers = request.get("headers")
|
|
756
|
+
has_auth = isinstance(headers, dict) and any(
|
|
757
|
+
str(key).strip().lower() == "authorization" and str(value).strip()
|
|
758
|
+
for key, value in headers.items()
|
|
759
|
+
)
|
|
760
|
+
if not has_auth:
|
|
761
|
+
raise ProviderError(
|
|
762
|
+
"x402-validate",
|
|
763
|
+
(
|
|
764
|
+
"Alchemy's x402 gateway needs wallet-auth headers in addition to the payment challenge. "
|
|
765
|
+
"The generic x402 tool does not mint Alchemy SIWE/SIWS auth tokens yet, so this endpoint "
|
|
766
|
+
"is not safe to execute through the generic flow."
|
|
767
|
+
),
|
|
768
|
+
details={
|
|
769
|
+
"request_url": request.get("url"),
|
|
770
|
+
"host": host,
|
|
771
|
+
"wallet_chain": _backend_chain(backend),
|
|
772
|
+
"wallet_network": _backend_network(backend),
|
|
773
|
+
"hint": "Use a dedicated Alchemy agent gateway integration or authenticated CLI flow.",
|
|
774
|
+
},
|
|
775
|
+
)
|
|
776
|
+
|
|
674
777
|
def _select_sdk_payment_requirement(
|
|
675
778
|
payment_required: Any,
|
|
676
779
|
*,
|
|
@@ -940,15 +1043,57 @@ async def _create_payment_headers(
|
|
|
940
1043
|
)
|
|
941
1044
|
|
|
942
1045
|
|
|
943
|
-
def _extract_settlement_header(response: Any) -> dict[str, Any]
|
|
1046
|
+
def _extract_settlement_header(response: Any) -> dict[str, Any]:
|
|
944
1047
|
sdk = _load_x402_sdk()
|
|
1048
|
+
settle = sdk["x402HTTPClientBase"]().get_payment_settle_response(
|
|
1049
|
+
lambda name: response.headers.get(name)
|
|
1050
|
+
)
|
|
1051
|
+
return settle.model_dump(by_alias=True, exclude_none=True)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def _extract_settlement_header_safe(response: Any) -> dict[str, Any] | None:
|
|
945
1055
|
try:
|
|
946
|
-
|
|
947
|
-
|
|
1056
|
+
return _extract_settlement_header(response)
|
|
1057
|
+
except Exception as exc:
|
|
1058
|
+
log.warning(
|
|
1059
|
+
"x402 settlement header parse failed",
|
|
1060
|
+
extra={
|
|
1061
|
+
"status_code": getattr(response, "status_code", None),
|
|
1062
|
+
"payment_response": response.headers.get("PAYMENT-RESPONSE")
|
|
1063
|
+
if hasattr(response, "headers")
|
|
1064
|
+
else None,
|
|
1065
|
+
"x_payment_response": response.headers.get("X-PAYMENT-RESPONSE")
|
|
1066
|
+
if hasattr(response, "headers")
|
|
1067
|
+
else None,
|
|
1068
|
+
"error_type": type(exc).__name__,
|
|
1069
|
+
"error": str(exc) or None,
|
|
1070
|
+
},
|
|
948
1071
|
)
|
|
949
|
-
except Exception:
|
|
950
1072
|
return None
|
|
951
|
-
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def _log_x402_execute(
|
|
1076
|
+
*,
|
|
1077
|
+
request: dict[str, Any],
|
|
1078
|
+
selected_payment: dict[str, Any] | None,
|
|
1079
|
+
response: Any,
|
|
1080
|
+
settlement: dict[str, Any] | None,
|
|
1081
|
+
) -> None:
|
|
1082
|
+
log.info(
|
|
1083
|
+
"x402 execute completed",
|
|
1084
|
+
extra={
|
|
1085
|
+
"url": request.get("url"),
|
|
1086
|
+
"method": request.get("method"),
|
|
1087
|
+
"request_fingerprint": request.get("request_fingerprint"),
|
|
1088
|
+
"x402_network": selected_payment.get("network") if isinstance(selected_payment, dict) else None,
|
|
1089
|
+
"x402_asset": selected_payment.get("asset") if isinstance(selected_payment, dict) else None,
|
|
1090
|
+
"x402_amount": selected_payment.get("amount") if isinstance(selected_payment, dict) else None,
|
|
1091
|
+
"x402_pay_to": selected_payment.get("pay_to") if isinstance(selected_payment, dict) else None,
|
|
1092
|
+
"status_code": getattr(response, "status_code", None),
|
|
1093
|
+
"transaction": settlement.get("transaction") if isinstance(settlement, dict) else None,
|
|
1094
|
+
"confirmed": bool(settlement and settlement.get("success")),
|
|
1095
|
+
},
|
|
1096
|
+
)
|
|
952
1097
|
|
|
953
1098
|
|
|
954
1099
|
async def search_services(
|
|
@@ -1250,6 +1395,29 @@ async def execute_request(
|
|
|
1250
1395
|
query: dict[str, Any] | None = None,
|
|
1251
1396
|
json_body: Any | None = None,
|
|
1252
1397
|
text_body: str | None = None,
|
|
1398
|
+
) -> dict[str, Any]:
|
|
1399
|
+
executed = await pay_and_fetch(
|
|
1400
|
+
backend=backend,
|
|
1401
|
+
url=url,
|
|
1402
|
+
method=method,
|
|
1403
|
+
headers=headers,
|
|
1404
|
+
query=query,
|
|
1405
|
+
json_body=json_body,
|
|
1406
|
+
text_body=text_body,
|
|
1407
|
+
)
|
|
1408
|
+
executed["mode"] = "execute"
|
|
1409
|
+
return executed
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
async def pay_and_fetch(
|
|
1413
|
+
*,
|
|
1414
|
+
backend: AgentWalletBackend,
|
|
1415
|
+
url: str,
|
|
1416
|
+
method: str = "GET",
|
|
1417
|
+
headers: dict[str, Any] | None = None,
|
|
1418
|
+
query: dict[str, Any] | None = None,
|
|
1419
|
+
json_body: Any | None = None,
|
|
1420
|
+
text_body: str | None = None,
|
|
1253
1421
|
) -> dict[str, Any]:
|
|
1254
1422
|
preview = await preview_request(
|
|
1255
1423
|
backend=backend,
|
|
@@ -1268,7 +1436,13 @@ async def execute_request(
|
|
|
1268
1436
|
executed["confirmed"] = False
|
|
1269
1437
|
return executed
|
|
1270
1438
|
|
|
1271
|
-
selected_payment =
|
|
1439
|
+
selected_payment = _validate_payment_requirement(
|
|
1440
|
+
preview.get("selected_payment")
|
|
1441
|
+
if isinstance(preview.get("selected_payment"), dict)
|
|
1442
|
+
else None,
|
|
1443
|
+
backend=backend,
|
|
1444
|
+
request_url=str(preview.get("request_url") or url),
|
|
1445
|
+
)
|
|
1272
1446
|
payment_required_header = (
|
|
1273
1447
|
dict(preview.get("response_headers") or {}).get("payment-required")
|
|
1274
1448
|
)
|
|
@@ -1283,15 +1457,26 @@ async def execute_request(
|
|
|
1283
1457
|
json_body=json_body,
|
|
1284
1458
|
text_body=text_body,
|
|
1285
1459
|
)
|
|
1460
|
+
_validate_request_execution_policy(request=request, backend=backend)
|
|
1286
1461
|
payment_headers = await _create_payment_headers(
|
|
1287
1462
|
backend=backend,
|
|
1288
1463
|
payment_required_header=payment_required_header,
|
|
1289
1464
|
selected_payment=selected_payment,
|
|
1290
1465
|
)
|
|
1291
|
-
payment_headers["Access-Control-Expose-Headers"] = "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE"
|
|
1292
1466
|
client = get_client()
|
|
1293
|
-
response = await _send_request(
|
|
1294
|
-
|
|
1467
|
+
response = await _send_request(
|
|
1468
|
+
client=client,
|
|
1469
|
+
request=request,
|
|
1470
|
+
extra_headers=payment_headers,
|
|
1471
|
+
timeout=X402_EXECUTE_TIMEOUT_SECONDS,
|
|
1472
|
+
)
|
|
1473
|
+
settlement = _extract_settlement_header_safe(response)
|
|
1474
|
+
_log_x402_execute(
|
|
1475
|
+
request=request,
|
|
1476
|
+
selected_payment=selected_payment,
|
|
1477
|
+
response=response,
|
|
1478
|
+
settlement=settlement,
|
|
1479
|
+
)
|
|
1295
1480
|
|
|
1296
1481
|
executed = dict(preview)
|
|
1297
1482
|
executed.update(
|
|
@@ -33,6 +33,16 @@ def _lifi_chain_id_for_evm_network(network: str) -> str:
|
|
|
33
33
|
return "1"
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _normalize_x402_typed_data_value(value: Any) -> Any:
|
|
37
|
+
if isinstance(value, (bytes, bytearray)):
|
|
38
|
+
return "0x" + bytes(value).hex()
|
|
39
|
+
if isinstance(value, list):
|
|
40
|
+
return [_normalize_x402_typed_data_value(item) for item in value]
|
|
41
|
+
if isinstance(value, dict):
|
|
42
|
+
return {str(key): _normalize_x402_typed_data_value(item) for key, item in value.items()}
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
36
46
|
def _extract_fee_wei(payload: dict[str, Any]) -> str | None:
|
|
37
47
|
for key in ("fee", "maxFee", "totalFee", "gasCost", "cost"):
|
|
38
48
|
value = payload.get(key)
|
|
@@ -457,7 +467,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
|
|
|
457
467
|
"domain": domain,
|
|
458
468
|
"types": types,
|
|
459
469
|
"primaryType": primary_type,
|
|
460
|
-
"message": message,
|
|
470
|
+
"message": _normalize_x402_typed_data_value(message),
|
|
461
471
|
},
|
|
462
472
|
)
|
|
463
473
|
signature = str(data.get("signature") or "").strip()
|