@agentlayer.tech/wallet 0.1.26 → 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.
@@ -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] | None:
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
- settle = sdk["x402HTTPClientBase"]().get_payment_settle_response(
947
- lambda name: response.headers.get(name)
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
- return settle.model_dump(by_alias=True, exclude_none=True)
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 = _require_executable_payment(preview=preview, backend=backend)
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(client=client, request=request, extra_headers=payment_headers)
1294
- settlement = _extract_settlement_header(response)
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(
@@ -718,6 +718,7 @@ def verify_provider_kamino_lend_transaction(
718
718
  market_address: str,
719
719
  reserve_address: str,
720
720
  action: str,
721
+ obligation_address: str | None = None,
721
722
  loaded_addresses: list[str] | None = None,
722
723
  ) -> dict[str, Any]:
723
724
  binding = _assert_basic_wallet_binding(
@@ -734,6 +735,10 @@ def verify_provider_kamino_lend_transaction(
734
735
  raise WalletBackendError(
735
736
  f"{action} transaction does not reference the expected Kamino reserve."
736
737
  )
738
+ if obligation_address and obligation_address not in keys:
739
+ raise WalletBackendError(
740
+ f"{action} transaction does not reference the expected Kamino obligation."
741
+ )
737
742
  program_ids = _program_ids(message, loaded_addresses)
738
743
  unknown_program_ids = _assert_program_allowlist(
739
744
  program_ids,
@@ -764,6 +769,7 @@ def verify_provider_kamino_lend_transaction(
764
769
  "instruction_count": len(_compiled_instructions(message)),
765
770
  "market_address": market_address,
766
771
  "reserve_address": reserve_address,
772
+ "obligation_address": obligation_address,
767
773
  "action": action,
768
774
  "verified": True,
769
775
  }
@@ -525,6 +525,7 @@ class AgentWalletBackend(ABC):
525
525
  market: str,
526
526
  reserve: str,
527
527
  amount_ui: str,
528
+ obligation_address: str | None = None,
528
529
  ) -> dict[str, Any]:
529
530
  raise WalletBackendError(f"{self.name} does not support Kamino deposit previews.")
530
531
 
@@ -533,6 +534,8 @@ class AgentWalletBackend(ABC):
533
534
  market: str,
534
535
  reserve: str,
535
536
  amount_ui: str,
537
+ obligation_address: str | None = None,
538
+ approved_preview: dict[str, Any] | None = None,
536
539
  ) -> dict[str, Any]:
537
540
  raise WalletBackendError(f"{self.name} does not support Kamino deposit preparation.")
538
541
 
@@ -541,6 +544,8 @@ class AgentWalletBackend(ABC):
541
544
  market: str,
542
545
  reserve: str,
543
546
  amount_ui: str,
547
+ obligation_address: str | None = None,
548
+ approved_preview: dict[str, Any] | None = None,
544
549
  ) -> dict[str, Any]:
545
550
  raise WalletBackendError(f"{self.name} does not support Kamino deposits.")
546
551
 
@@ -549,6 +554,7 @@ class AgentWalletBackend(ABC):
549
554
  market: str,
550
555
  reserve: str,
551
556
  amount_ui: str,
557
+ obligation_address: str | None = None,
552
558
  ) -> dict[str, Any]:
553
559
  raise WalletBackendError(f"{self.name} does not support Kamino withdraw previews.")
554
560
 
@@ -557,6 +563,8 @@ class AgentWalletBackend(ABC):
557
563
  market: str,
558
564
  reserve: str,
559
565
  amount_ui: str,
566
+ obligation_address: str | None = None,
567
+ approved_preview: dict[str, Any] | None = None,
560
568
  ) -> dict[str, Any]:
561
569
  raise WalletBackendError(f"{self.name} does not support Kamino withdraw preparation.")
562
570
 
@@ -565,6 +573,8 @@ class AgentWalletBackend(ABC):
565
573
  market: str,
566
574
  reserve: str,
567
575
  amount_ui: str,
576
+ obligation_address: str | None = None,
577
+ approved_preview: dict[str, Any] | None = None,
568
578
  ) -> dict[str, Any]:
569
579
  raise WalletBackendError(f"{self.name} does not support Kamino withdraws.")
570
580
 
@@ -573,6 +583,7 @@ class AgentWalletBackend(ABC):
573
583
  market: str,
574
584
  reserve: str,
575
585
  amount_ui: str,
586
+ obligation_address: str | None = None,
576
587
  ) -> dict[str, Any]:
577
588
  raise WalletBackendError(f"{self.name} does not support Kamino borrow previews.")
578
589
 
@@ -581,6 +592,8 @@ class AgentWalletBackend(ABC):
581
592
  market: str,
582
593
  reserve: str,
583
594
  amount_ui: str,
595
+ obligation_address: str | None = None,
596
+ approved_preview: dict[str, Any] | None = None,
584
597
  ) -> dict[str, Any]:
585
598
  raise WalletBackendError(f"{self.name} does not support Kamino borrow preparation.")
586
599
 
@@ -589,6 +602,8 @@ class AgentWalletBackend(ABC):
589
602
  market: str,
590
603
  reserve: str,
591
604
  amount_ui: str,
605
+ obligation_address: str | None = None,
606
+ approved_preview: dict[str, Any] | None = None,
592
607
  ) -> dict[str, Any]:
593
608
  raise WalletBackendError(f"{self.name} does not support Kamino borrows.")
594
609
 
@@ -597,6 +612,7 @@ class AgentWalletBackend(ABC):
597
612
  market: str,
598
613
  reserve: str,
599
614
  amount_ui: str,
615
+ obligation_address: str | None = None,
600
616
  ) -> dict[str, Any]:
601
617
  raise WalletBackendError(f"{self.name} does not support Kamino repay previews.")
602
618
 
@@ -605,6 +621,8 @@ class AgentWalletBackend(ABC):
605
621
  market: str,
606
622
  reserve: str,
607
623
  amount_ui: str,
624
+ obligation_address: str | None = None,
625
+ approved_preview: dict[str, Any] | None = None,
608
626
  ) -> dict[str, Any]:
609
627
  raise WalletBackendError(f"{self.name} does not support Kamino repay preparation.")
610
628
 
@@ -613,6 +631,8 @@ class AgentWalletBackend(ABC):
613
631
  market: str,
614
632
  reserve: str,
615
633
  amount_ui: str,
634
+ obligation_address: str | None = None,
635
+ approved_preview: dict[str, Any] | None = None,
616
636
  ) -> dict[str, Any]:
617
637
  raise WalletBackendError(f"{self.name} does not support Kamino repays.")
618
638