Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions live/guild/tests_x402_interop/fake_facilitator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""
from __future__ import annotations

import copy
import hashlib
import threading
import time
Expand All @@ -26,6 +27,10 @@

app = FastAPI(title="fake-x402-facilitator")
_settled: dict[str, str] = {}
_bazaar_captures: dict[str, dict[str, Any] | None] = {
"verify": None,
"settle": None,
}
_lock = threading.Lock()


Expand All @@ -35,6 +40,25 @@ class FacilitatorRequest(BaseModel):
paymentRequirements: dict[str, Any]


def reset_bazaar_captures() -> None:
"""Clear facilitator-observed Bazaar metadata between test payments."""
with _lock:
_bazaar_captures["verify"] = None
_bazaar_captures["settle"] = None


def bazaar_captures() -> dict[str, dict[str, Any] | None]:
"""Return an isolated snapshot of metadata received on both legs."""
with _lock:
return copy.deepcopy(_bazaar_captures)


def _capture_bazaar(leg: str, body: FacilitatorRequest) -> None:
extensions = body.paymentPayload.get("extensions") or {}
with _lock:
_bazaar_captures[leg] = copy.deepcopy(extensions.get("bazaar"))


def _recover_signer(reqs: dict[str, Any], auth: dict[str, Any],
signature: str) -> str:
extra = reqs.get("extra") or {}
Expand Down Expand Up @@ -110,6 +134,7 @@ def verify(body: FacilitatorRequest):
ok, reason, payer = _validate(body)
if not ok:
return {"isValid": False, "invalidReason": reason, "payer": payer or None}
_capture_bazaar("verify", body)
return {"isValid": True, "payer": payer}


Expand All @@ -119,6 +144,7 @@ def settle(body: FacilitatorRequest):
if not ok:
return {"success": False, "errorReason": reason, "payer": payer or None,
"transaction": "", "network": NETWORK}
_capture_bazaar("settle", body)
nonce = body.paymentPayload["payload"]["authorization"]["nonce"]
with _lock:
if nonce in _settled: # EIP-3009 contracts reject nonce reuse
Expand Down
16 changes: 16 additions & 0 deletions live/guild/tests_x402_interop/test_official_client_interop.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ def test_unpaid_request_gets_v2_challenge(live_stack):


def test_official_client_pays_and_gets_result_and_receipt(live_stack):
unpaid = httpx.get(live_stack["guild"] + PAID_PATH)
assert unpaid.status_code == 402
challenge = json.loads(base64.b64decode(
unpaid.headers[PAYMENT_REQUIRED_HEADER]))
quoted_bazaar = challenge["extensions"]["bazaar"]

from fake_facilitator import bazaar_captures, reset_bazaar_captures
reset_bazaar_captures()

async def run():
transport = x402_httpx_transport(_official_client())
async with httpx.AsyncClient(transport=transport) as http:
Expand All @@ -95,6 +104,13 @@ async def run():
payer = Account.from_key(CLIENT_KEY).address
assert receipt.payer and receipt.payer.lower() == payer.lower()

# CDP Bazaar indexing depends on the client echoing the server-declared
# extension into the PaymentPayload that reaches BOTH facilitator legs.
# Prove exact preservation, rather than merely checking the unpaid 402.
captured_bazaar = bazaar_captures()
assert captured_bazaar["verify"] == quoted_bazaar
assert captured_bazaar["settle"] == quoted_bazaar

# the service records the settlement with its full identity, and REAL
# revenue stays zero — this was a value-less testnet-shaped settlement
revenue = httpx.get(live_stack["guild"] + "/billing/revenue").json()
Expand Down