Skip to content

Commit bcc752a

Browse files
vvillait88claude
andcommitted
flatten stripe_multichain helpers to kwargs; delete 4 wrappers
Flattens the 4 stripe_multichain builders: - create_multichain_payment_intent(CreateMultichainPaymentIntentInput(...)) → create_multichain_payment_intent(*, stripe, amount, currency="usd", networks=None, metadata=None, idempotency_key=None) - create_pi_cache(PiCacheOptions(...)) → create_pi_cache(*, redis_url=None, ttl_seconds=300, key_prefix="payto:") - simulate_crypto_deposit(SimulateCryptoDepositInput(...)) → simulate_crypto_deposit(*, payment_intent_id, network, stripe_secret_key, buyer_wallet=None, ...) - simulate_deposit_if_test_mode(SimulateDepositIfTestModeInput(...)) → simulate_deposit_if_test_mode(*, get_payment_intent_id, deposit_address, network, stripe_secret_key, ...) Deleted from public exports: CreateMultichainPaymentIntentInput, PiCacheOptions, SimulateCryptoDepositInput, SimulateDepositIfTestModeInput. Kept (consumers pattern-match): PiCache, MultichainPaymentIntentResult, StripeClientLike. Tests: 1033 passed / 3 skipped, 95.15% coverage. Added 2 fill tests covering optional kwargs (`transaction_hash` / `stripe_version` / `extra`) and the error-swallow branch in `simulate_deposit_if_test_mode` so coverage stays above the 95% floor after the dataclass-init lines went away. ty + ruff clean. vulture: only known false positives (string-cast, Protocol-method-param) remain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4396e9b commit bcc752a

8 files changed

Lines changed: 164 additions & 164 deletions

File tree

agentscore_commerce/stripe_multichain/__init__.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,16 @@
55
create_mppx_stripe,
66
)
77
from agentscore_commerce.stripe_multichain.payment_intent import (
8-
CreateMultichainPaymentIntentInput,
98
MultichainPaymentIntentResult,
109
StripeClientLike,
1110
create_multichain_payment_intent,
1211
get_deposit_address,
1312
)
14-
from agentscore_commerce.stripe_multichain.pi_cache import (
15-
PiCache,
16-
PiCacheOptions,
17-
create_pi_cache,
18-
)
13+
from agentscore_commerce.stripe_multichain.pi_cache import PiCache, create_pi_cache
1914
from agentscore_commerce.stripe_multichain.simulate_deposit import (
2015
DEFAULT_BUYER_WALLET,
2116
STRIPE_TEST_TX_HASH_FAILED,
2217
STRIPE_TEST_TX_HASH_SUCCESS,
23-
SimulateCryptoDepositInput,
24-
SimulateDepositIfTestModeInput,
2518
simulate_crypto_deposit,
2619
simulate_deposit_if_test_mode,
2720
)
@@ -31,12 +24,8 @@
3124
"DEFAULT_PAYMENT_METHOD_TYPES",
3225
"STRIPE_TEST_TX_HASH_FAILED",
3326
"STRIPE_TEST_TX_HASH_SUCCESS",
34-
"CreateMultichainPaymentIntentInput",
3527
"MultichainPaymentIntentResult",
3628
"PiCache",
37-
"PiCacheOptions",
38-
"SimulateCryptoDepositInput",
39-
"SimulateDepositIfTestModeInput",
4029
"StripeClientLike",
4130
"create_mppx_stripe",
4231
"create_multichain_payment_intent",

agentscore_commerce/stripe_multichain/payment_intent.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
chains, returning the PI id + deposit addresses per network. Distinct from the Stripe SPT flow.
55
"""
66

7-
from dataclasses import dataclass, field
7+
from dataclasses import dataclass
88
from typing import Any, Protocol
99

1010

@@ -16,44 +16,46 @@ class StripeClientLike(Protocol):
1616
payment_intents: StripePaymentIntentsAPI
1717

1818

19-
@dataclass
20-
class CreateMultichainPaymentIntentInput:
21-
stripe: Any # StripeClientLike, but kept loose so vendors can pass their actual `stripe.StripeClient`
22-
amount: int # in cents (Stripe convention)
23-
currency: str = "usd"
24-
networks: list[str] = field(default_factory=lambda: ["tempo", "base", "solana"])
25-
metadata: dict[str, str] | None = None
26-
idempotency_key: str | None = None
27-
28-
2919
@dataclass
3020
class MultichainPaymentIntentResult:
3121
payment_intent_id: str
3222
deposit_addresses: dict[str, str]
3323

3424

35-
def create_multichain_payment_intent(input: CreateMultichainPaymentIntentInput) -> MultichainPaymentIntentResult:
25+
_DEFAULT_NETWORKS: tuple[str, ...] = ("tempo", "base", "solana")
26+
27+
28+
def create_multichain_payment_intent(
29+
*,
30+
stripe: Any, # StripeClientLike, kept loose so vendors can pass their actual `stripe.StripeClient`
31+
amount: int, # in cents (Stripe convention)
32+
currency: str = "usd",
33+
networks: list[str] | None = None,
34+
metadata: dict[str, str] | None = None,
35+
idempotency_key: str | None = None,
36+
) -> MultichainPaymentIntentResult:
3637
"""Create a Stripe PaymentIntent with multichain crypto deposit_options.
3738
3839
Returns the PI id + per-network deposit addresses. Raises if Stripe doesn't return any addresses.
3940
"""
41+
resolved_networks = list(networks) if networks else list(_DEFAULT_NETWORKS)
4042
params: dict[str, Any] = {
41-
"amount": input.amount,
42-
"currency": input.currency,
43+
"amount": amount,
44+
"currency": currency,
4345
"payment_method_types": ["crypto"],
4446
"payment_method_data": {"type": "crypto"},
4547
"payment_method_options": {
46-
"crypto": {"mode": "deposit", "deposit_options": {"networks": input.networks}},
48+
"crypto": {"mode": "deposit", "deposit_options": {"networks": resolved_networks}},
4749
},
4850
"confirm": True,
4951
}
50-
if input.metadata:
51-
params["metadata"] = input.metadata
52+
if metadata:
53+
params["metadata"] = metadata
5254

5355
pi = (
54-
input.stripe.payment_intents.create(params, idempotency_key=input.idempotency_key)
55-
if input.idempotency_key
56-
else input.stripe.payment_intents.create(params)
56+
stripe.payment_intents.create(params, idempotency_key=idempotency_key)
57+
if idempotency_key
58+
else stripe.payment_intents.create(params)
5759
)
5860
deposit_addresses: dict[str, str] = {}
5961
next_action = getattr(pi, "next_action", None) or (pi.get("next_action") if isinstance(pi, dict) else None)

agentscore_commerce/stripe_multichain/pi_cache.py

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,6 @@ class _Entry(Generic[T]):
5252
expires_at: float
5353

5454

55-
@dataclass
56-
class PiCacheOptions:
57-
"""Optional configuration for :func:`create_pi_cache`."""
58-
59-
#: Redis connection URL (e.g. ``rediss://…cache.amazonaws.com:6379``). When omitted,
60-
#: the cache falls back to in-process dicts with the same API.
61-
redis_url: str | None = None
62-
#: TTL for cached entries in seconds. Default 300.
63-
ttl_seconds: int = 300
64-
#: Prefix for Redis keys. Default ``'payto:'``.
65-
key_prefix: str = "payto:"
66-
67-
6855
@dataclass
6956
class PiCache:
7057
"""Stripe PI + deposit-address cache produced by :func:`create_pi_cache`."""
@@ -78,17 +65,25 @@ class PiCache:
7865
stop: Callable[[], None]
7966

8067

81-
def create_pi_cache(opts: PiCacheOptions | None = None) -> PiCache:
68+
def create_pi_cache(
69+
*,
70+
redis_url: str | None = None,
71+
ttl_seconds: int = 300,
72+
key_prefix: str = "payto:",
73+
) -> PiCache:
8274
"""Construct a Stripe PI + deposit-address cache instance.
8375
8476
Returns a ``PiCache`` with async ``cache_address`` / ``has_address`` (Redis-backed
8577
when ``redis_url`` is set) and sync helpers for PI-id and network-address lookup.
8678
A background task evicts expired in-memory entries every 60 seconds; call
8779
``stop()`` from server shutdown handlers to cancel it.
80+
81+
``redis_url`` — connection URL (e.g. ``rediss://…cache.amazonaws.com:6379``); when
82+
omitted, the cache falls back to in-process dicts with the same API.
83+
``ttl_seconds`` — entry TTL (default 300).
84+
``key_prefix`` — Redis key prefix (default ``'payto:'``).
8885
"""
89-
options = opts or PiCacheOptions()
90-
ttl = options.ttl_seconds
91-
key_prefix = options.key_prefix
86+
ttl = ttl_seconds
9287

9388
redis_client: _RedisLike | None = None
9489
addr_mem_cache: dict[str, float] = {}
@@ -97,7 +92,7 @@ def create_pi_cache(opts: PiCacheOptions | None = None) -> PiCache:
9792

9893
async def _get_redis() -> _RedisLike | None:
9994
nonlocal redis_client
100-
if not options.redis_url:
95+
if not redis_url:
10196
return None
10297
if redis_client is not None:
10398
return redis_client
@@ -112,7 +107,7 @@ async def _get_redis() -> _RedisLike | None:
112107
"[pi-cache] redis_url set but `redis` is not installed. Run `pip install redis` or unset redis_url."
113108
)
114109
return None
115-
redis_client = redis_asyncio.from_url(options.redis_url)
110+
redis_client = redis_asyncio.from_url(redis_url)
116111
return redis_client
117112

118113
async def cache_address(address: str) -> None:

agentscore_commerce/stripe_multichain/simulate_deposit.py

Lines changed: 47 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import logging
44
from collections.abc import Callable
5-
from dataclasses import dataclass, field
65
from typing import Literal
76

87
import httpx
@@ -28,57 +27,52 @@
2827
STRIPE_TEST_TX_HASH_FAILED = "0x000000000000000000000000000000000000000000000000000000testfailed"
2928

3029

31-
@dataclass
32-
class SimulateCryptoDepositInput:
33-
payment_intent_id: str
34-
network: Literal["tempo", "base", "solana"]
35-
stripe_secret_key: str
36-
buyer_wallet: str | None = None
37-
token_currency: str | None = None
38-
transaction_hash: str | None = None
39-
stripe_version: str | None = None
40-
stripe_api_base: str = "https://api.stripe.com"
41-
extra: dict[str, str] = field(default_factory=dict)
42-
43-
44-
async def simulate_crypto_deposit(input: SimulateCryptoDepositInput) -> None:
30+
async def simulate_crypto_deposit(
31+
*,
32+
payment_intent_id: str,
33+
network: Literal["tempo", "base", "solana"],
34+
stripe_secret_key: str,
35+
buyer_wallet: str | None = None,
36+
token_currency: str | None = None,
37+
transaction_hash: str | None = None,
38+
stripe_version: str | None = None,
39+
stripe_api_base: str = "https://api.stripe.com",
40+
extra: dict[str, str] | None = None,
41+
) -> None:
4542
"""Call Stripe's `test_helpers/payment_intents/{id}/simulate_crypto_deposit` endpoint."""
46-
url = f"{input.stripe_api_base}/v1/test_helpers/payment_intents/{input.payment_intent_id}/simulate_crypto_deposit"
43+
url = f"{stripe_api_base}/v1/test_helpers/payment_intents/{payment_intent_id}/simulate_crypto_deposit"
4744
params: dict[str, str] = {
48-
"network": input.network,
49-
"buyer_wallet": input.buyer_wallet or DEFAULT_BUYER_WALLET.get(input.network, ""),
45+
"network": network,
46+
"buyer_wallet": buyer_wallet or DEFAULT_BUYER_WALLET.get(network, ""),
5047
}
51-
if input.token_currency:
52-
params["token_currency"] = input.token_currency
53-
if input.transaction_hash:
54-
params["transaction_hash"] = input.transaction_hash
55-
params.update(input.extra)
48+
if token_currency:
49+
params["token_currency"] = token_currency
50+
if transaction_hash:
51+
params["transaction_hash"] = transaction_hash
52+
if extra:
53+
params.update(extra)
5654
headers: dict[str, str] = {
57-
"Authorization": f"Bearer {input.stripe_secret_key}",
55+
"Authorization": f"Bearer {stripe_secret_key}",
5856
"Content-Type": "application/x-www-form-urlencoded",
5957
}
60-
if input.stripe_version:
61-
headers["Stripe-Version"] = input.stripe_version
58+
if stripe_version:
59+
headers["Stripe-Version"] = stripe_version
6260
async with httpx.AsyncClient() as client:
6361
res = await client.post(url, headers=headers, content="&".join(f"{k}={v}" for k, v in params.items()))
6462
if res.status_code >= 300:
6563
raise RuntimeError(f"Stripe simulate_crypto_deposit failed: {res.status_code} {res.text}")
6664

6765

68-
@dataclass
69-
class SimulateDepositIfTestModeInput:
70-
"""Input for :func:`simulate_deposit_if_test_mode`."""
71-
72-
get_payment_intent_id: Callable[[str], str | None]
73-
deposit_address: str
74-
network: Literal["tempo", "base", "solana"]
75-
stripe_secret_key: str
76-
buyer_wallet: str | None = None
77-
token_currency: str = "usdc"
78-
stripe_version: str | None = None
79-
80-
81-
async def simulate_deposit_if_test_mode(input: SimulateDepositIfTestModeInput) -> None:
66+
async def simulate_deposit_if_test_mode(
67+
*,
68+
get_payment_intent_id: Callable[[str], str | None],
69+
deposit_address: str,
70+
network: Literal["tempo", "base", "solana"],
71+
stripe_secret_key: str,
72+
buyer_wallet: str | None = None,
73+
token_currency: str = "usdc", # noqa: S107 — literal default, not a secret
74+
stripe_version: str | None = None,
75+
) -> None:
8276
"""Higher-level wrapper around :func:`simulate_crypto_deposit` for the testnet/dev path.
8377
8478
Bundles the three steps every Stripe-multichain merchant repeats:
@@ -94,34 +88,32 @@ async def simulate_deposit_if_test_mode(input: SimulateDepositIfTestModeInput) -
9488
9589
Use case is exclusively dev/testnet end-to-end — production servers (sk_live_) no-op.
9690
"""
97-
if not input.stripe_secret_key.startswith("sk_test_"):
91+
if not stripe_secret_key.startswith("sk_test_"):
9892
return
99-
pi_id = input.get_payment_intent_id(input.deposit_address)
93+
pi_id = get_payment_intent_id(deposit_address)
10094
if not pi_id:
10195
logger.warning(
10296
"[stripe] Skipping deposit simulation — no PI cached for deposit address %s… (network=%s). "
10397
"The PI cache TTL may have expired between 402 emission and settlement.",
104-
input.deposit_address[:10],
105-
input.network,
98+
deposit_address[:10],
99+
network,
106100
)
107101
return
108102
try:
109103
await simulate_crypto_deposit(
110-
SimulateCryptoDepositInput(
111-
payment_intent_id=pi_id,
112-
network=input.network,
113-
stripe_secret_key=input.stripe_secret_key,
114-
buyer_wallet=input.buyer_wallet,
115-
token_currency=input.token_currency,
116-
transaction_hash=STRIPE_TEST_TX_HASH_SUCCESS,
117-
stripe_version=input.stripe_version,
118-
)
104+
payment_intent_id=pi_id,
105+
network=network,
106+
stripe_secret_key=stripe_secret_key,
107+
buyer_wallet=buyer_wallet,
108+
token_currency=token_currency,
109+
transaction_hash=STRIPE_TEST_TX_HASH_SUCCESS,
110+
stripe_version=stripe_version,
119111
)
120-
logger.warning("[stripe] ✓ Simulated %s deposit for PI %s", input.network, pi_id)
112+
logger.warning("[stripe] ✓ Simulated %s deposit for PI %s", network, pi_id)
121113
except Exception as err:
122114
logger.error(
123115
"[stripe] ✗ Failed to simulate %s deposit for PI %s: %s",
124-
input.network,
116+
network,
125117
pi_id,
126118
err,
127119
)

examples/multi_rail_merchant.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@
5858
verify_x402_request,
5959
)
6060
from agentscore_commerce.stripe_multichain import (
61-
PiCacheOptions,
62-
SimulateDepositIfTestModeInput,
6361
create_pi_cache,
6462
simulate_deposit_if_test_mode,
6563
)
@@ -75,7 +73,7 @@
7573
# Singleton Stripe PI / deposit-address cache. Backed by Redis when REDIS_URL is set
7674
# (multi-instance deployments need this so a deposit lands on whichever instance
7775
# settles it); falls back to in-process dict for single-instance dev.
78-
pi_cache = create_pi_cache(PiCacheOptions(redis_url=os.environ.get("REDIS_URL")))
76+
pi_cache = create_pi_cache(redis_url=os.environ.get("REDIS_URL"))
7977

8078
app = FastAPI()
8179
_gate = AgentScoreGate(
@@ -168,12 +166,10 @@ async def purchase(request: Request, assess: dict = Depends(get_agentscore_data)
168166
# Fire Stripe testnet sim; no-ops on live keys. x402 settle only ever
169167
# lands on base in 1.4+ (Solana moved to MPP `solana/charge`).
170168
await simulate_deposit_if_test_mode(
171-
SimulateDepositIfTestModeInput(
172-
get_payment_intent_id=pi_cache.get_payment_intent_id,
173-
deposit_address=verified.signed_pay_to,
174-
network="base",
175-
stripe_secret_key=os.environ["STRIPE_SECRET_KEY"],
176-
)
169+
get_payment_intent_id=pi_cache.get_payment_intent_id,
170+
deposit_address=verified.signed_pay_to,
171+
network="base",
172+
stripe_secret_key=os.environ["STRIPE_SECRET_KEY"],
177173
)
178174

179175
headers: dict[str, str] = {}

0 commit comments

Comments
 (0)