uvd-x402-sdk 0.5.6__py3-none-any.whl → 0.7.0__py3-none-any.whl

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.
@@ -0,0 +1,633 @@
1
+ """
2
+ Advanced Escrow client for x402 PaymentOperator integration.
3
+
4
+ This module provides the 5 Advanced Escrow flows via the PaymentOperator contract:
5
+ 1. AUTHORIZE - Lock funds in escrow (via facilitator)
6
+ 2. RELEASE - Capture escrowed funds to receiver (on-chain)
7
+ 3. REFUND IN ESCROW - Return escrowed funds to payer (on-chain)
8
+ 4. CHARGE - Direct instant payment without escrow (on-chain)
9
+ 5. REFUND POST ESCROW - Dispute refund after release (NOT FUNCTIONAL - tokenCollector not implemented)
10
+
11
+ Contract deposit limit: $100 USDC per deposit (enforced on-chain).
12
+ Dispute resolution: use refund_in_escrow() (keep funds in escrow, arbiter decides).
13
+
14
+ Contract mapping:
15
+ operator.authorize() -> escrow.authorize() (lock funds)
16
+ operator.release() -> escrow.capture() (pay receiver)
17
+ operator.refundInEscrow() -> escrow.partialVoid() (refund payer)
18
+ operator.charge() -> escrow.charge() (direct payment)
19
+ operator.refundPostEscrow() -> escrow.refund() (dispute refund)
20
+
21
+ Example:
22
+ >>> from uvd_x402_sdk.advanced_escrow import AdvancedEscrowClient
23
+ >>>
24
+ >>> client = AdvancedEscrowClient(
25
+ ... facilitator_url="https://facilitator.ultravioletadao.xyz",
26
+ ... rpc_url="https://mainnet.base.org",
27
+ ... private_key="0x...",
28
+ ... chain_id=8453,
29
+ ... )
30
+ >>>
31
+ >>> # Build payment info
32
+ >>> pi = client.build_payment_info(
33
+ ... receiver="0xWorker...",
34
+ ... amount=5_000_000, # $5 USDC
35
+ ... tier=TaskTier.STANDARD,
36
+ ... )
37
+ >>>
38
+ >>> # Lock funds in escrow
39
+ >>> auth = client.authorize(pi)
40
+ >>>
41
+ >>> # After work is done, release to worker
42
+ >>> tx = client.release(auth.payment_info)
43
+ >>>
44
+ >>> # Or cancel and refund
45
+ >>> tx = client.refund_in_escrow(auth.payment_info)
46
+ """
47
+
48
+ import secrets
49
+ import time
50
+ from dataclasses import dataclass, field
51
+ from enum import Enum
52
+ from typing import Optional
53
+
54
+ import httpx
55
+ from eth_abi import encode
56
+ from eth_account import Account
57
+ from eth_account.messages import encode_typed_data
58
+ from web3 import Web3
59
+
60
+
61
+ # ============================================================
62
+ # Constants
63
+ # ============================================================
64
+
65
+ PAYMENT_INFO_TYPEHASH = bytes.fromhex(
66
+ "ae68ac7ce30c86ece8196b61a7c486d8f0061f575037fbd34e7fe4e2820c6591"
67
+ )
68
+
69
+ ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
70
+
71
+ # Contract deposit limit (enforced by PaymentOperator condition).
72
+ # As of 2026-02-03, commerce-payments contracts enforce $100 max per deposit.
73
+ DEPOSIT_LIMIT_USDC = 100_000_000 # $100 in atomic units (6 decimals)
74
+
75
+ # Base Mainnet contract addresses (default)
76
+ BASE_MAINNET_CONTRACTS = {
77
+ "operator": "0xa06958D93135BEd7e43893897C0d9fA931EF051C",
78
+ "escrow": "0x320a3c35F131E5D2Fb36af56345726B298936037",
79
+ "token_collector": "0x32d6AC59BCe8DFB3026F10BcaDB8D00AB218f5b6",
80
+ "usdc": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
81
+ }
82
+
83
+ # PaymentOperator ABI (minimal, for the 5 functions we need)
84
+ OPERATOR_ABI = [
85
+ {
86
+ "type": "function",
87
+ "name": "release",
88
+ "inputs": [
89
+ {
90
+ "name": "paymentInfo",
91
+ "type": "tuple",
92
+ "components": [
93
+ {"name": "operator", "type": "address"},
94
+ {"name": "payer", "type": "address"},
95
+ {"name": "receiver", "type": "address"},
96
+ {"name": "token", "type": "address"},
97
+ {"name": "maxAmount", "type": "uint120"},
98
+ {"name": "preApprovalExpiry", "type": "uint48"},
99
+ {"name": "authorizationExpiry", "type": "uint48"},
100
+ {"name": "refundExpiry", "type": "uint48"},
101
+ {"name": "minFeeBps", "type": "uint16"},
102
+ {"name": "maxFeeBps", "type": "uint16"},
103
+ {"name": "feeReceiver", "type": "address"},
104
+ {"name": "salt", "type": "uint256"},
105
+ ],
106
+ },
107
+ {"name": "amount", "type": "uint256"},
108
+ ],
109
+ "outputs": [],
110
+ "stateMutability": "nonpayable",
111
+ },
112
+ {
113
+ "type": "function",
114
+ "name": "refundInEscrow",
115
+ "inputs": [
116
+ {
117
+ "name": "paymentInfo",
118
+ "type": "tuple",
119
+ "components": [
120
+ {"name": "operator", "type": "address"},
121
+ {"name": "payer", "type": "address"},
122
+ {"name": "receiver", "type": "address"},
123
+ {"name": "token", "type": "address"},
124
+ {"name": "maxAmount", "type": "uint120"},
125
+ {"name": "preApprovalExpiry", "type": "uint48"},
126
+ {"name": "authorizationExpiry", "type": "uint48"},
127
+ {"name": "refundExpiry", "type": "uint48"},
128
+ {"name": "minFeeBps", "type": "uint16"},
129
+ {"name": "maxFeeBps", "type": "uint16"},
130
+ {"name": "feeReceiver", "type": "address"},
131
+ {"name": "salt", "type": "uint256"},
132
+ ],
133
+ },
134
+ {"name": "amount", "type": "uint120"},
135
+ ],
136
+ "outputs": [],
137
+ "stateMutability": "nonpayable",
138
+ },
139
+ {
140
+ "type": "function",
141
+ "name": "charge",
142
+ "inputs": [
143
+ {
144
+ "name": "paymentInfo",
145
+ "type": "tuple",
146
+ "components": [
147
+ {"name": "operator", "type": "address"},
148
+ {"name": "payer", "type": "address"},
149
+ {"name": "receiver", "type": "address"},
150
+ {"name": "token", "type": "address"},
151
+ {"name": "maxAmount", "type": "uint120"},
152
+ {"name": "preApprovalExpiry", "type": "uint48"},
153
+ {"name": "authorizationExpiry", "type": "uint48"},
154
+ {"name": "refundExpiry", "type": "uint48"},
155
+ {"name": "minFeeBps", "type": "uint16"},
156
+ {"name": "maxFeeBps", "type": "uint16"},
157
+ {"name": "feeReceiver", "type": "address"},
158
+ {"name": "salt", "type": "uint256"},
159
+ ],
160
+ },
161
+ {"name": "amount", "type": "uint256"},
162
+ {"name": "tokenCollector", "type": "address"},
163
+ {"name": "collectorData", "type": "bytes"},
164
+ ],
165
+ "outputs": [],
166
+ "stateMutability": "nonpayable",
167
+ },
168
+ {
169
+ "type": "function",
170
+ "name": "refundPostEscrow",
171
+ "inputs": [
172
+ {
173
+ "name": "paymentInfo",
174
+ "type": "tuple",
175
+ "components": [
176
+ {"name": "operator", "type": "address"},
177
+ {"name": "payer", "type": "address"},
178
+ {"name": "receiver", "type": "address"},
179
+ {"name": "token", "type": "address"},
180
+ {"name": "maxAmount", "type": "uint120"},
181
+ {"name": "preApprovalExpiry", "type": "uint48"},
182
+ {"name": "authorizationExpiry", "type": "uint48"},
183
+ {"name": "refundExpiry", "type": "uint48"},
184
+ {"name": "minFeeBps", "type": "uint16"},
185
+ {"name": "maxFeeBps", "type": "uint16"},
186
+ {"name": "feeReceiver", "type": "address"},
187
+ {"name": "salt", "type": "uint256"},
188
+ ],
189
+ },
190
+ {"name": "amount", "type": "uint256"},
191
+ {"name": "tokenCollector", "type": "address"},
192
+ {"name": "collectorData", "type": "bytes"},
193
+ ],
194
+ "outputs": [],
195
+ "stateMutability": "nonpayable",
196
+ },
197
+ ]
198
+
199
+
200
+ # ============================================================
201
+ # Types
202
+ # ============================================================
203
+
204
+
205
+ class TaskTier(str, Enum):
206
+ """Chamba task tier determines timing parameters."""
207
+
208
+ MICRO = "micro" # $0.50-$5: 1h accept, 2h complete, 24h dispute
209
+ STANDARD = "standard" # $5-$50: 2h accept, 24h complete, 7d dispute
210
+ PREMIUM = "premium" # $50-$200: 4h accept, 48h complete, 14d dispute
211
+ ENTERPRISE = "enterprise" # $200+: 24h accept, 7d complete, 30d dispute
212
+
213
+
214
+ TIER_TIMINGS = {
215
+ TaskTier.MICRO: {"pre": 3600, "auth": 7200, "refund": 86400},
216
+ TaskTier.STANDARD: {"pre": 7200, "auth": 86400, "refund": 604800},
217
+ TaskTier.PREMIUM: {"pre": 14400, "auth": 172800, "refund": 1209600},
218
+ TaskTier.ENTERPRISE: {"pre": 86400, "auth": 604800, "refund": 2592000},
219
+ }
220
+
221
+
222
+ @dataclass
223
+ class PaymentInfo:
224
+ """PaymentInfo struct matching the on-chain PaymentOperator contract."""
225
+
226
+ operator: str
227
+ receiver: str
228
+ token: str
229
+ max_amount: int
230
+ pre_approval_expiry: int
231
+ authorization_expiry: int
232
+ refund_expiry: int
233
+ min_fee_bps: int = 0
234
+ max_fee_bps: int = 800
235
+ fee_receiver: str = ""
236
+ salt: str = field(default_factory=lambda: "0x" + secrets.token_hex(32))
237
+
238
+ def __post_init__(self):
239
+ if not self.fee_receiver:
240
+ self.fee_receiver = self.operator
241
+
242
+
243
+ @dataclass
244
+ class AuthorizationResult:
245
+ """Result of an AUTHORIZE operation."""
246
+
247
+ success: bool
248
+ transaction_hash: Optional[str] = None
249
+ payment_info: Optional[PaymentInfo] = None
250
+ salt: Optional[str] = None
251
+ error: Optional[str] = None
252
+
253
+
254
+ @dataclass
255
+ class TransactionResult:
256
+ """Result of an on-chain transaction."""
257
+
258
+ success: bool
259
+ transaction_hash: Optional[str] = None
260
+ gas_used: Optional[int] = None
261
+ error: Optional[str] = None
262
+
263
+
264
+ # ============================================================
265
+ # Client
266
+ # ============================================================
267
+
268
+
269
+ class AdvancedEscrowClient:
270
+ """
271
+ Client for x402 Advanced Escrow (PaymentOperator) operations.
272
+
273
+ Provides the 5 escrow flows:
274
+ - authorize(): Lock funds in escrow via facilitator
275
+ - release(): Capture escrowed funds to receiver
276
+ - refund_in_escrow(): Return escrowed funds to payer
277
+ - charge(): Direct instant payment (no escrow)
278
+ - refund_post_escrow(): Dispute refund after release
279
+ """
280
+
281
+ def __init__(
282
+ self,
283
+ private_key: str,
284
+ *,
285
+ facilitator_url: str = "https://facilitator.ultravioletadao.xyz",
286
+ rpc_url: str = "https://mainnet.base.org",
287
+ chain_id: int = 8453,
288
+ contracts: Optional[dict] = None,
289
+ gas_limit: int = 300000,
290
+ ):
291
+ self.private_key = private_key
292
+ self.facilitator_url = facilitator_url.rstrip("/")
293
+ self.chain_id = chain_id
294
+ self.gas_limit = gas_limit
295
+ self.contracts = contracts or BASE_MAINNET_CONTRACTS
296
+ self.w3 = Web3(Web3.HTTPProvider(rpc_url))
297
+ self.account = Account.from_key(private_key)
298
+ self.payer = self.account.address
299
+
300
+ self.operator_contract = self.w3.eth.contract(
301
+ address=Web3.to_checksum_address(self.contracts["operator"]),
302
+ abi=OPERATOR_ABI,
303
+ )
304
+
305
+ def _compute_nonce(self, payment_info: PaymentInfo) -> str:
306
+ """Compute the correct nonce (with PAYMENT_INFO_TYPEHASH)."""
307
+ salt = payment_info.salt
308
+ if isinstance(salt, str):
309
+ salt = int(salt, 16) if salt.startswith("0x") else int(salt)
310
+
311
+ pi_tuple = (
312
+ Web3.to_checksum_address(payment_info.operator),
313
+ ZERO_ADDRESS, # payer = 0 for payer-agnostic hash
314
+ Web3.to_checksum_address(payment_info.receiver),
315
+ Web3.to_checksum_address(payment_info.token),
316
+ payment_info.max_amount,
317
+ payment_info.pre_approval_expiry,
318
+ payment_info.authorization_expiry,
319
+ payment_info.refund_expiry,
320
+ payment_info.min_fee_bps,
321
+ payment_info.max_fee_bps,
322
+ Web3.to_checksum_address(payment_info.fee_receiver),
323
+ salt,
324
+ )
325
+
326
+ encoded_with_typehash = encode(
327
+ [
328
+ "bytes32",
329
+ "(address,address,address,address,uint120,uint48,uint48,uint48,uint16,uint16,address,uint256)",
330
+ ],
331
+ [PAYMENT_INFO_TYPEHASH, pi_tuple],
332
+ )
333
+ pi_hash = Web3.keccak(encoded_with_typehash)
334
+
335
+ final_encoded = encode(
336
+ ["uint256", "address", "bytes32"],
337
+ [self.chain_id, Web3.to_checksum_address(self.contracts["escrow"]), pi_hash],
338
+ )
339
+ return "0x" + Web3.keccak(final_encoded).hex()
340
+
341
+ def _sign_erc3009(self, auth: dict) -> str:
342
+ """Sign ReceiveWithAuthorization for ERC-3009."""
343
+ domain = {
344
+ "name": "USD Coin",
345
+ "version": "2",
346
+ "chainId": self.chain_id,
347
+ "verifyingContract": Web3.to_checksum_address(self.contracts["usdc"]),
348
+ }
349
+ types = {
350
+ "ReceiveWithAuthorization": [
351
+ {"name": "from", "type": "address"},
352
+ {"name": "to", "type": "address"},
353
+ {"name": "value", "type": "uint256"},
354
+ {"name": "validAfter", "type": "uint256"},
355
+ {"name": "validBefore", "type": "uint256"},
356
+ {"name": "nonce", "type": "bytes32"},
357
+ ],
358
+ }
359
+ message = {
360
+ "from": Web3.to_checksum_address(auth["from"]),
361
+ "to": Web3.to_checksum_address(auth["to"]),
362
+ "value": int(auth["value"]),
363
+ "validAfter": int(auth["validAfter"]),
364
+ "validBefore": int(auth["validBefore"]),
365
+ "nonce": auth["nonce"],
366
+ }
367
+ signable = encode_typed_data(domain_data=domain, message_types=types, message_data=message)
368
+ signed = self.account.sign_message(signable)
369
+ return "0x" + signed.signature.hex()
370
+
371
+ def _build_tuple(self, pi: PaymentInfo) -> tuple:
372
+ """Build the on-chain PaymentInfo tuple."""
373
+ if isinstance(pi.salt, int):
374
+ salt_int = pi.salt
375
+ elif isinstance(pi.salt, str):
376
+ salt_int = int(pi.salt, 16) if pi.salt.startswith("0x") else int(pi.salt, 16) if all(c in "0123456789abcdefABCDEF" for c in pi.salt) else int(pi.salt)
377
+ else:
378
+ salt_int = int(pi.salt)
379
+ return (
380
+ Web3.to_checksum_address(pi.operator),
381
+ Web3.to_checksum_address(self.payer),
382
+ Web3.to_checksum_address(pi.receiver),
383
+ Web3.to_checksum_address(pi.token),
384
+ pi.max_amount,
385
+ pi.pre_approval_expiry,
386
+ pi.authorization_expiry,
387
+ pi.refund_expiry,
388
+ pi.min_fee_bps,
389
+ pi.max_fee_bps,
390
+ Web3.to_checksum_address(pi.fee_receiver),
391
+ salt_int,
392
+ )
393
+
394
+ def _send_tx(self, func_call) -> TransactionResult:
395
+ """Build, sign, and send a transaction."""
396
+ try:
397
+ gas_price = self.w3.eth.gas_price
398
+ tx = func_call.build_transaction({
399
+ "from": self.payer,
400
+ "nonce": self.w3.eth.get_transaction_count(self.payer),
401
+ "gas": self.gas_limit,
402
+ "maxFeePerGas": gas_price * 2,
403
+ "maxPriorityFeePerGas": gas_price,
404
+ })
405
+ signed = self.w3.eth.account.sign_transaction(tx, self.private_key)
406
+ tx_hash = self.w3.eth.send_raw_transaction(signed.raw_transaction)
407
+ receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
408
+
409
+ if receipt["status"] != 1:
410
+ return TransactionResult(success=False, transaction_hash=tx_hash.hex(), gas_used=receipt["gasUsed"], error="Transaction reverted")
411
+
412
+ return TransactionResult(success=True, transaction_hash=tx_hash.hex(), gas_used=receipt["gasUsed"])
413
+ except Exception as e:
414
+ return TransactionResult(success=False, error=str(e))
415
+
416
+ def build_payment_info(
417
+ self,
418
+ receiver: str,
419
+ amount: int,
420
+ *,
421
+ tier: TaskTier = TaskTier.STANDARD,
422
+ salt: Optional[str] = None,
423
+ min_fee_bps: int = 0,
424
+ max_fee_bps: int = 800,
425
+ ) -> PaymentInfo:
426
+ """
427
+ Build a PaymentInfo struct with appropriate timing for the task tier.
428
+
429
+ Args:
430
+ receiver: Worker's wallet address
431
+ amount: Amount in token atomic units (e.g., 5_000_000 for $5 USDC)
432
+ tier: Task tier (determines timing parameters)
433
+ salt: Random salt (auto-generated if not provided)
434
+ min_fee_bps: Minimum fee in basis points
435
+ max_fee_bps: Maximum fee in basis points
436
+ """
437
+ now = int(time.time())
438
+ t = TIER_TIMINGS[tier]
439
+
440
+ return PaymentInfo(
441
+ operator=self.contracts["operator"],
442
+ receiver=receiver,
443
+ token=self.contracts["usdc"],
444
+ max_amount=amount,
445
+ pre_approval_expiry=now + t["pre"],
446
+ authorization_expiry=now + t["auth"],
447
+ refund_expiry=now + t["refund"],
448
+ min_fee_bps=min_fee_bps,
449
+ max_fee_bps=max_fee_bps,
450
+ fee_receiver=self.contracts["operator"],
451
+ salt=salt or ("0x" + secrets.token_hex(32)),
452
+ )
453
+
454
+ def authorize(self, payment_info: PaymentInfo) -> AuthorizationResult:
455
+ """
456
+ AUTHORIZE: Lock funds in escrow via the facilitator.
457
+
458
+ This sends an ERC-3009 ReceiveWithAuthorization to the facilitator,
459
+ which calls PaymentOperator.authorize() on-chain.
460
+
461
+ Args:
462
+ payment_info: PaymentInfo struct with timing and amount
463
+
464
+ Returns:
465
+ AuthorizationResult with transaction hash
466
+ """
467
+ nonce = self._compute_nonce(payment_info)
468
+
469
+ auth = {
470
+ "from": self.payer,
471
+ "to": self.contracts["token_collector"],
472
+ "value": str(payment_info.max_amount),
473
+ "validAfter": "0",
474
+ "validBefore": str(payment_info.pre_approval_expiry),
475
+ "nonce": nonce,
476
+ }
477
+ signature = self._sign_erc3009(auth)
478
+
479
+ pi_dict = {
480
+ "operator": payment_info.operator,
481
+ "receiver": payment_info.receiver,
482
+ "token": payment_info.token,
483
+ "maxAmount": str(payment_info.max_amount),
484
+ "preApprovalExpiry": payment_info.pre_approval_expiry,
485
+ "authorizationExpiry": payment_info.authorization_expiry,
486
+ "refundExpiry": payment_info.refund_expiry,
487
+ "minFeeBps": payment_info.min_fee_bps,
488
+ "maxFeeBps": payment_info.max_fee_bps,
489
+ "feeReceiver": payment_info.fee_receiver,
490
+ "salt": payment_info.salt,
491
+ }
492
+
493
+ payload = {
494
+ "x402Version": 2,
495
+ "scheme": "escrow",
496
+ "payload": {
497
+ "authorization": auth,
498
+ "signature": signature,
499
+ "paymentInfo": pi_dict,
500
+ },
501
+ "paymentRequirements": {
502
+ "scheme": "escrow",
503
+ "network": f"eip155:{self.chain_id}",
504
+ "maxAmountRequired": str(payment_info.max_amount),
505
+ "asset": self.contracts["usdc"],
506
+ "payTo": payment_info.receiver,
507
+ "extra": {
508
+ "escrowAddress": self.contracts["escrow"],
509
+ "operatorAddress": self.contracts["operator"],
510
+ "tokenCollector": self.contracts["token_collector"],
511
+ },
512
+ },
513
+ }
514
+
515
+ try:
516
+ response = httpx.post(
517
+ f"{self.facilitator_url}/settle",
518
+ json=payload,
519
+ timeout=120,
520
+ )
521
+ result = response.json()
522
+
523
+ if result.get("success"):
524
+ return AuthorizationResult(
525
+ success=True,
526
+ transaction_hash=result.get("transaction"),
527
+ payment_info=payment_info,
528
+ salt=payment_info.salt,
529
+ )
530
+ else:
531
+ return AuthorizationResult(success=False, error=result.get("errorReason"))
532
+ except Exception as e:
533
+ return AuthorizationResult(success=False, error=str(e))
534
+
535
+ def release(self, payment_info: PaymentInfo, amount: Optional[int] = None) -> TransactionResult:
536
+ """
537
+ RELEASE: Capture escrowed funds to receiver (worker gets paid).
538
+
539
+ Calls PaymentOperator.release() -> escrow.capture()
540
+
541
+ Args:
542
+ payment_info: PaymentInfo from the authorize step
543
+ amount: Amount to release (defaults to max_amount)
544
+ """
545
+ pt = self._build_tuple(payment_info)
546
+ amt = amount or payment_info.max_amount
547
+ return self._send_tx(self.operator_contract.functions.release(pt, amt))
548
+
549
+ def refund_in_escrow(self, payment_info: PaymentInfo, amount: Optional[int] = None) -> TransactionResult:
550
+ """
551
+ REFUND IN ESCROW: Return escrowed funds to payer (cancel task).
552
+
553
+ Calls PaymentOperator.refundInEscrow() -> escrow.partialVoid()
554
+
555
+ Args:
556
+ payment_info: PaymentInfo from the authorize step
557
+ amount: Amount to refund (defaults to max_amount)
558
+ """
559
+ pt = self._build_tuple(payment_info)
560
+ amt = amount or payment_info.max_amount
561
+ return self._send_tx(self.operator_contract.functions.refundInEscrow(pt, amt))
562
+
563
+ def charge(self, payment_info: PaymentInfo, amount: Optional[int] = None) -> TransactionResult:
564
+ """
565
+ CHARGE: Direct instant payment (no escrow hold).
566
+
567
+ Calls PaymentOperator.charge() -> escrow.charge()
568
+ Funds go directly from payer to receiver.
569
+
570
+ Args:
571
+ payment_info: PaymentInfo with receiver and amount
572
+ amount: Amount to charge (defaults to max_amount)
573
+ """
574
+ nonce = self._compute_nonce(payment_info)
575
+ amt = amount or payment_info.max_amount
576
+
577
+ auth = {
578
+ "from": self.payer,
579
+ "to": self.contracts["token_collector"],
580
+ "value": str(amt),
581
+ "validAfter": "0",
582
+ "validBefore": str(payment_info.pre_approval_expiry),
583
+ "nonce": nonce,
584
+ }
585
+ signature = self._sign_erc3009(auth)
586
+ collector_data = bytes.fromhex(signature[2:])
587
+
588
+ pt = self._build_tuple(payment_info)
589
+ return self._send_tx(
590
+ self.operator_contract.functions.charge(
591
+ pt, amt,
592
+ Web3.to_checksum_address(self.contracts["token_collector"]),
593
+ collector_data,
594
+ )
595
+ )
596
+
597
+ def refund_post_escrow(
598
+ self,
599
+ payment_info: PaymentInfo,
600
+ amount: Optional[int] = None,
601
+ token_collector: str = ZERO_ADDRESS,
602
+ collector_data: bytes = b"",
603
+ ) -> TransactionResult:
604
+ """
605
+ REFUND POST ESCROW: Dispute refund after funds were released.
606
+
607
+ Calls PaymentOperator.refundPostEscrow() -> escrow.refund()
608
+
609
+ WARNING: NOT FUNCTIONAL IN PRODUCTION (as of 2026-02-03).
610
+ The protocol team has not implemented the required tokenCollector
611
+ contract. This call will fail on-chain.
612
+
613
+ For dispute resolution, use refund_in_escrow() instead: keep funds
614
+ in escrow and refund before releasing. This guarantees funds are
615
+ available and under arbiter control.
616
+
617
+ Kept for future use when tokenCollector is implemented.
618
+
619
+ Args:
620
+ payment_info: PaymentInfo from the original authorization
621
+ amount: Amount to refund (defaults to max_amount)
622
+ token_collector: Address of token collector for refund sourcing
623
+ collector_data: Data for the token collector
624
+ """
625
+ pt = self._build_tuple(payment_info)
626
+ amt = amount or payment_info.max_amount
627
+ return self._send_tx(
628
+ self.operator_contract.functions.refundPostEscrow(
629
+ pt, amt,
630
+ Web3.to_checksum_address(token_collector),
631
+ collector_data,
632
+ )
633
+ )