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.
- uvd_x402_sdk/__init__.py +348 -241
- uvd_x402_sdk/advanced_escrow.py +633 -0
- uvd_x402_sdk/erc8004.py +663 -0
- uvd_x402_sdk/escrow.py +637 -0
- uvd_x402_sdk/networks/__init__.py +9 -9
- uvd_x402_sdk/networks/base.py +3 -0
- uvd_x402_sdk/networks/evm.py +71 -1
- {uvd_x402_sdk-0.5.6.dist-info → uvd_x402_sdk-0.7.0.dist-info}/METADATA +313 -9
- {uvd_x402_sdk-0.5.6.dist-info → uvd_x402_sdk-0.7.0.dist-info}/RECORD +12 -9
- {uvd_x402_sdk-0.5.6.dist-info → uvd_x402_sdk-0.7.0.dist-info}/WHEEL +1 -1
- {uvd_x402_sdk-0.5.6.dist-info → uvd_x402_sdk-0.7.0.dist-info}/LICENSE +0 -0
- {uvd_x402_sdk-0.5.6.dist-info → uvd_x402_sdk-0.7.0.dist-info}/top_level.txt +0 -0
uvd_x402_sdk/escrow.py
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Escrow and Refund client for x402 SDK.
|
|
3
|
+
|
|
4
|
+
This module provides escrow payment functionality with refund and dispute
|
|
5
|
+
resolution capabilities. Payments can be held in escrow until service delivery
|
|
6
|
+
is confirmed, with options for refunds and dispute arbitration.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Create escrow payments
|
|
10
|
+
- Release funds to recipients
|
|
11
|
+
- Request and process refunds
|
|
12
|
+
- Open and resolve disputes
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from uvd_x402_sdk.escrow import EscrowClient
|
|
16
|
+
>>>
|
|
17
|
+
>>> client = EscrowClient()
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Create escrow payment
|
|
20
|
+
>>> escrow = await client.create_escrow(
|
|
21
|
+
... payment_header="...",
|
|
22
|
+
... requirements={...},
|
|
23
|
+
... escrow_duration=86400, # 24 hours
|
|
24
|
+
... )
|
|
25
|
+
>>>
|
|
26
|
+
>>> # After service delivery, release funds
|
|
27
|
+
>>> await client.release(escrow.id)
|
|
28
|
+
>>>
|
|
29
|
+
>>> # Or if service failed, request refund
|
|
30
|
+
>>> await client.request_refund(
|
|
31
|
+
... escrow_id=escrow.id,
|
|
32
|
+
... reason="Service not delivered",
|
|
33
|
+
... )
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from enum import Enum
|
|
37
|
+
from typing import Any, Literal, Optional
|
|
38
|
+
|
|
39
|
+
import httpx
|
|
40
|
+
from pydantic import BaseModel, Field
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EscrowStatus(str, Enum):
|
|
44
|
+
"""Escrow payment status."""
|
|
45
|
+
|
|
46
|
+
PENDING = "pending" # Payment initiated, awaiting confirmation
|
|
47
|
+
HELD = "held" # Funds held in escrow
|
|
48
|
+
RELEASED = "released" # Funds released to recipient
|
|
49
|
+
REFUNDED = "refunded" # Funds returned to payer
|
|
50
|
+
DISPUTED = "disputed" # Dispute in progress
|
|
51
|
+
EXPIRED = "expired" # Escrow expired without resolution
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RefundStatus(str, Enum):
|
|
55
|
+
"""Refund request status."""
|
|
56
|
+
|
|
57
|
+
PENDING = "pending" # Refund requested, awaiting processing
|
|
58
|
+
APPROVED = "approved" # Refund approved
|
|
59
|
+
REJECTED = "rejected" # Refund rejected
|
|
60
|
+
PROCESSED = "processed" # Refund completed on-chain
|
|
61
|
+
DISPUTED = "disputed" # Under dispute review
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DisputeOutcome(str, Enum):
|
|
65
|
+
"""Dispute resolution outcome."""
|
|
66
|
+
|
|
67
|
+
PENDING = "pending" # Dispute under review
|
|
68
|
+
PAYER_WINS = "payer_wins" # Payer gets refund
|
|
69
|
+
RECIPIENT_WINS = "recipient_wins" # Recipient keeps funds
|
|
70
|
+
SPLIT = "split" # Funds split between parties
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ReleaseConditions(BaseModel):
|
|
74
|
+
"""Conditions for releasing escrow funds."""
|
|
75
|
+
|
|
76
|
+
min_hold_time: Optional[int] = Field(None, alias="minHoldTime")
|
|
77
|
+
confirmations: Optional[int] = None
|
|
78
|
+
custom: Optional[Any] = None
|
|
79
|
+
|
|
80
|
+
class Config:
|
|
81
|
+
populate_by_name = True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class EscrowPayment(BaseModel):
|
|
85
|
+
"""Escrow payment record."""
|
|
86
|
+
|
|
87
|
+
id: str
|
|
88
|
+
payment_header: str = Field(..., alias="paymentHeader")
|
|
89
|
+
status: EscrowStatus
|
|
90
|
+
network: str
|
|
91
|
+
payer: str
|
|
92
|
+
recipient: str
|
|
93
|
+
amount: str
|
|
94
|
+
asset: str
|
|
95
|
+
resource: str
|
|
96
|
+
expires_at: str = Field(..., alias="expiresAt")
|
|
97
|
+
release_conditions: Optional[ReleaseConditions] = Field(None, alias="releaseConditions")
|
|
98
|
+
transaction_hash: Optional[str] = Field(None, alias="transactionHash")
|
|
99
|
+
created_at: str = Field(..., alias="createdAt")
|
|
100
|
+
updated_at: str = Field(..., alias="updatedAt")
|
|
101
|
+
|
|
102
|
+
class Config:
|
|
103
|
+
populate_by_name = True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RefundResponse(BaseModel):
|
|
107
|
+
"""Refund response from recipient/facilitator."""
|
|
108
|
+
|
|
109
|
+
status: Literal["approved", "rejected"]
|
|
110
|
+
reason: Optional[str] = None
|
|
111
|
+
responded_at: str = Field(..., alias="respondedAt")
|
|
112
|
+
|
|
113
|
+
class Config:
|
|
114
|
+
populate_by_name = True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class RefundRequest(BaseModel):
|
|
118
|
+
"""Refund request record."""
|
|
119
|
+
|
|
120
|
+
id: str
|
|
121
|
+
escrow_id: str = Field(..., alias="escrowId")
|
|
122
|
+
status: RefundStatus
|
|
123
|
+
reason: str
|
|
124
|
+
evidence: Optional[str] = None
|
|
125
|
+
amount_requested: str = Field(..., alias="amountRequested")
|
|
126
|
+
amount_approved: Optional[str] = Field(None, alias="amountApproved")
|
|
127
|
+
requester: str
|
|
128
|
+
transaction_hash: Optional[str] = Field(None, alias="transactionHash")
|
|
129
|
+
response: Optional[RefundResponse] = None
|
|
130
|
+
created_at: str = Field(..., alias="createdAt")
|
|
131
|
+
updated_at: str = Field(..., alias="updatedAt")
|
|
132
|
+
|
|
133
|
+
class Config:
|
|
134
|
+
populate_by_name = True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Dispute(BaseModel):
|
|
138
|
+
"""Dispute record."""
|
|
139
|
+
|
|
140
|
+
id: str
|
|
141
|
+
escrow_id: str = Field(..., alias="escrowId")
|
|
142
|
+
refund_request_id: Optional[str] = Field(None, alias="refundRequestId")
|
|
143
|
+
outcome: DisputeOutcome
|
|
144
|
+
initiator: Literal["payer", "recipient"]
|
|
145
|
+
reason: str
|
|
146
|
+
payer_evidence: Optional[str] = Field(None, alias="payerEvidence")
|
|
147
|
+
recipient_evidence: Optional[str] = Field(None, alias="recipientEvidence")
|
|
148
|
+
arbitration_notes: Optional[str] = Field(None, alias="arbitrationNotes")
|
|
149
|
+
payer_amount: Optional[str] = Field(None, alias="payerAmount")
|
|
150
|
+
recipient_amount: Optional[str] = Field(None, alias="recipientAmount")
|
|
151
|
+
transaction_hashes: Optional[list[str]] = Field(None, alias="transactionHashes")
|
|
152
|
+
created_at: str = Field(..., alias="createdAt")
|
|
153
|
+
resolved_at: Optional[str] = Field(None, alias="resolvedAt")
|
|
154
|
+
|
|
155
|
+
class Config:
|
|
156
|
+
populate_by_name = True
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class EscrowListResponse(BaseModel):
|
|
160
|
+
"""Paginated list of escrow payments."""
|
|
161
|
+
|
|
162
|
+
escrows: list[EscrowPayment]
|
|
163
|
+
total: int
|
|
164
|
+
page: int
|
|
165
|
+
limit: int
|
|
166
|
+
has_more: bool = Field(..., alias="hasMore")
|
|
167
|
+
|
|
168
|
+
class Config:
|
|
169
|
+
populate_by_name = True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class EscrowClient:
|
|
173
|
+
"""
|
|
174
|
+
Client for x402 Escrow & Refund operations.
|
|
175
|
+
|
|
176
|
+
The Escrow system holds payments until service is verified,
|
|
177
|
+
enabling refunds and dispute resolution.
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> client = EscrowClient()
|
|
181
|
+
>>>
|
|
182
|
+
>>> # Create escrow payment (backend)
|
|
183
|
+
>>> escrow = await client.create_escrow(
|
|
184
|
+
... payment_header=request.headers["x-payment"],
|
|
185
|
+
... requirements=payment_requirements,
|
|
186
|
+
... escrow_duration=86400, # 24 hours
|
|
187
|
+
... )
|
|
188
|
+
>>>
|
|
189
|
+
>>> # After service is provided, release the escrow
|
|
190
|
+
>>> await client.release(escrow.id)
|
|
191
|
+
>>>
|
|
192
|
+
>>> # If service not provided, payer can request refund
|
|
193
|
+
>>> await client.request_refund(
|
|
194
|
+
... escrow_id=escrow.id,
|
|
195
|
+
... reason="Service not delivered within expected timeframe",
|
|
196
|
+
... )
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
base_url: str = "https://escrow.ultravioletadao.xyz",
|
|
202
|
+
api_key: Optional[str] = None,
|
|
203
|
+
timeout: float = 30.0,
|
|
204
|
+
):
|
|
205
|
+
"""
|
|
206
|
+
Initialize the Escrow client.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
base_url: Base URL of the Escrow API
|
|
210
|
+
api_key: API key for authenticated operations
|
|
211
|
+
timeout: Request timeout in seconds
|
|
212
|
+
"""
|
|
213
|
+
self.base_url = base_url.rstrip("/")
|
|
214
|
+
self.api_key = api_key
|
|
215
|
+
self.timeout = timeout
|
|
216
|
+
self._client = httpx.AsyncClient(timeout=timeout)
|
|
217
|
+
|
|
218
|
+
async def __aenter__(self) -> "EscrowClient":
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
222
|
+
await self._client.aclose()
|
|
223
|
+
|
|
224
|
+
def _get_headers(self, authenticated: bool = False) -> dict[str, str]:
|
|
225
|
+
"""Get request headers."""
|
|
226
|
+
headers = {
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
"Accept": "application/json",
|
|
229
|
+
}
|
|
230
|
+
if authenticated and self.api_key:
|
|
231
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
232
|
+
return headers
|
|
233
|
+
|
|
234
|
+
async def create_escrow(
|
|
235
|
+
self,
|
|
236
|
+
payment_header: str,
|
|
237
|
+
requirements: dict[str, Any],
|
|
238
|
+
*,
|
|
239
|
+
escrow_duration: int = 86400,
|
|
240
|
+
release_conditions: Optional[dict[str, Any]] = None,
|
|
241
|
+
) -> EscrowPayment:
|
|
242
|
+
"""
|
|
243
|
+
Create an escrow payment.
|
|
244
|
+
|
|
245
|
+
Holds the payment in escrow until released or refunded.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
payment_header: Base64-encoded X-PAYMENT header
|
|
249
|
+
requirements: Payment requirements dict
|
|
250
|
+
escrow_duration: Escrow duration in seconds (default: 24h)
|
|
251
|
+
release_conditions: Optional release conditions
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Created escrow payment
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
httpx.HTTPStatusError: If the request fails
|
|
258
|
+
"""
|
|
259
|
+
url = f"{self.base_url}/escrow"
|
|
260
|
+
payload = {
|
|
261
|
+
"paymentHeader": payment_header,
|
|
262
|
+
"paymentRequirements": requirements,
|
|
263
|
+
"escrowDuration": escrow_duration,
|
|
264
|
+
}
|
|
265
|
+
if release_conditions:
|
|
266
|
+
payload["releaseConditions"] = release_conditions
|
|
267
|
+
|
|
268
|
+
response = await self._client.post(
|
|
269
|
+
url,
|
|
270
|
+
json=payload,
|
|
271
|
+
headers=self._get_headers(authenticated=True),
|
|
272
|
+
)
|
|
273
|
+
response.raise_for_status()
|
|
274
|
+
return EscrowPayment.model_validate(response.json())
|
|
275
|
+
|
|
276
|
+
async def get_escrow(self, escrow_id: str) -> EscrowPayment:
|
|
277
|
+
"""
|
|
278
|
+
Get escrow payment by ID.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
escrow_id: Escrow payment ID
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Escrow payment details
|
|
285
|
+
|
|
286
|
+
Raises:
|
|
287
|
+
httpx.HTTPStatusError: If the request fails
|
|
288
|
+
"""
|
|
289
|
+
url = f"{self.base_url}/escrow/{escrow_id}"
|
|
290
|
+
response = await self._client.get(url, headers=self._get_headers())
|
|
291
|
+
response.raise_for_status()
|
|
292
|
+
return EscrowPayment.model_validate(response.json())
|
|
293
|
+
|
|
294
|
+
async def release(self, escrow_id: str) -> EscrowPayment:
|
|
295
|
+
"""
|
|
296
|
+
Release escrow funds to recipient.
|
|
297
|
+
|
|
298
|
+
Call this after service has been successfully provided.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
escrow_id: Escrow payment ID
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Updated escrow payment with transaction hash
|
|
305
|
+
|
|
306
|
+
Raises:
|
|
307
|
+
httpx.HTTPStatusError: If the request fails
|
|
308
|
+
"""
|
|
309
|
+
url = f"{self.base_url}/escrow/{escrow_id}/release"
|
|
310
|
+
response = await self._client.post(
|
|
311
|
+
url,
|
|
312
|
+
headers=self._get_headers(authenticated=True),
|
|
313
|
+
)
|
|
314
|
+
response.raise_for_status()
|
|
315
|
+
return EscrowPayment.model_validate(response.json())
|
|
316
|
+
|
|
317
|
+
async def request_refund(
|
|
318
|
+
self,
|
|
319
|
+
escrow_id: str,
|
|
320
|
+
reason: str,
|
|
321
|
+
*,
|
|
322
|
+
amount: Optional[str] = None,
|
|
323
|
+
evidence: Optional[str] = None,
|
|
324
|
+
) -> RefundRequest:
|
|
325
|
+
"""
|
|
326
|
+
Request a refund for an escrow payment.
|
|
327
|
+
|
|
328
|
+
Initiates a refund request that must be approved.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
escrow_id: Escrow payment ID
|
|
332
|
+
reason: Reason for refund request
|
|
333
|
+
amount: Amount to refund (full amount if not specified)
|
|
334
|
+
evidence: Supporting evidence
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Created refund request
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
httpx.HTTPStatusError: If the request fails
|
|
341
|
+
"""
|
|
342
|
+
url = f"{self.base_url}/escrow/{escrow_id}/refund"
|
|
343
|
+
payload: dict[str, Any] = {"reason": reason}
|
|
344
|
+
if amount:
|
|
345
|
+
payload["amount"] = amount
|
|
346
|
+
if evidence:
|
|
347
|
+
payload["evidence"] = evidence
|
|
348
|
+
|
|
349
|
+
response = await self._client.post(
|
|
350
|
+
url,
|
|
351
|
+
json=payload,
|
|
352
|
+
headers=self._get_headers(authenticated=True),
|
|
353
|
+
)
|
|
354
|
+
response.raise_for_status()
|
|
355
|
+
return RefundRequest.model_validate(response.json())
|
|
356
|
+
|
|
357
|
+
async def approve_refund(
|
|
358
|
+
self,
|
|
359
|
+
refund_id: str,
|
|
360
|
+
amount: Optional[str] = None,
|
|
361
|
+
) -> RefundRequest:
|
|
362
|
+
"""
|
|
363
|
+
Approve a refund request (for recipients).
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
refund_id: Refund request ID
|
|
367
|
+
amount: Amount to approve (may be less than requested)
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Updated refund request
|
|
371
|
+
|
|
372
|
+
Raises:
|
|
373
|
+
httpx.HTTPStatusError: If the request fails
|
|
374
|
+
"""
|
|
375
|
+
url = f"{self.base_url}/refund/{refund_id}/approve"
|
|
376
|
+
payload: dict[str, Any] = {}
|
|
377
|
+
if amount:
|
|
378
|
+
payload["amount"] = amount
|
|
379
|
+
|
|
380
|
+
response = await self._client.post(
|
|
381
|
+
url,
|
|
382
|
+
json=payload,
|
|
383
|
+
headers=self._get_headers(authenticated=True),
|
|
384
|
+
)
|
|
385
|
+
response.raise_for_status()
|
|
386
|
+
return RefundRequest.model_validate(response.json())
|
|
387
|
+
|
|
388
|
+
async def reject_refund(self, refund_id: str, reason: str) -> RefundRequest:
|
|
389
|
+
"""
|
|
390
|
+
Reject a refund request (for recipients).
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
refund_id: Refund request ID
|
|
394
|
+
reason: Reason for rejection
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Updated refund request
|
|
398
|
+
|
|
399
|
+
Raises:
|
|
400
|
+
httpx.HTTPStatusError: If the request fails
|
|
401
|
+
"""
|
|
402
|
+
url = f"{self.base_url}/refund/{refund_id}/reject"
|
|
403
|
+
response = await self._client.post(
|
|
404
|
+
url,
|
|
405
|
+
json={"reason": reason},
|
|
406
|
+
headers=self._get_headers(authenticated=True),
|
|
407
|
+
)
|
|
408
|
+
response.raise_for_status()
|
|
409
|
+
return RefundRequest.model_validate(response.json())
|
|
410
|
+
|
|
411
|
+
async def get_refund(self, refund_id: str) -> RefundRequest:
|
|
412
|
+
"""
|
|
413
|
+
Get refund request by ID.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
refund_id: Refund request ID
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Refund request details
|
|
420
|
+
|
|
421
|
+
Raises:
|
|
422
|
+
httpx.HTTPStatusError: If the request fails
|
|
423
|
+
"""
|
|
424
|
+
url = f"{self.base_url}/refund/{refund_id}"
|
|
425
|
+
response = await self._client.get(url, headers=self._get_headers())
|
|
426
|
+
response.raise_for_status()
|
|
427
|
+
return RefundRequest.model_validate(response.json())
|
|
428
|
+
|
|
429
|
+
async def open_dispute(
|
|
430
|
+
self,
|
|
431
|
+
escrow_id: str,
|
|
432
|
+
reason: str,
|
|
433
|
+
evidence: Optional[str] = None,
|
|
434
|
+
) -> Dispute:
|
|
435
|
+
"""
|
|
436
|
+
Open a dispute for an escrow payment.
|
|
437
|
+
|
|
438
|
+
Initiates arbitration when payer and recipient disagree.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
escrow_id: Escrow payment ID
|
|
442
|
+
reason: Reason for dispute
|
|
443
|
+
evidence: Supporting evidence
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Created dispute
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
httpx.HTTPStatusError: If the request fails
|
|
450
|
+
"""
|
|
451
|
+
url = f"{self.base_url}/escrow/{escrow_id}/dispute"
|
|
452
|
+
payload: dict[str, Any] = {"reason": reason}
|
|
453
|
+
if evidence:
|
|
454
|
+
payload["evidence"] = evidence
|
|
455
|
+
|
|
456
|
+
response = await self._client.post(
|
|
457
|
+
url,
|
|
458
|
+
json=payload,
|
|
459
|
+
headers=self._get_headers(authenticated=True),
|
|
460
|
+
)
|
|
461
|
+
response.raise_for_status()
|
|
462
|
+
return Dispute.model_validate(response.json())
|
|
463
|
+
|
|
464
|
+
async def submit_evidence(self, dispute_id: str, evidence: str) -> Dispute:
|
|
465
|
+
"""
|
|
466
|
+
Submit evidence to a dispute.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
dispute_id: Dispute ID
|
|
470
|
+
evidence: Evidence to submit
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
Updated dispute
|
|
474
|
+
|
|
475
|
+
Raises:
|
|
476
|
+
httpx.HTTPStatusError: If the request fails
|
|
477
|
+
"""
|
|
478
|
+
url = f"{self.base_url}/dispute/{dispute_id}/evidence"
|
|
479
|
+
response = await self._client.post(
|
|
480
|
+
url,
|
|
481
|
+
json={"evidence": evidence},
|
|
482
|
+
headers=self._get_headers(authenticated=True),
|
|
483
|
+
)
|
|
484
|
+
response.raise_for_status()
|
|
485
|
+
return Dispute.model_validate(response.json())
|
|
486
|
+
|
|
487
|
+
async def get_dispute(self, dispute_id: str) -> Dispute:
|
|
488
|
+
"""
|
|
489
|
+
Get dispute by ID.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
dispute_id: Dispute ID
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Dispute details
|
|
496
|
+
|
|
497
|
+
Raises:
|
|
498
|
+
httpx.HTTPStatusError: If the request fails
|
|
499
|
+
"""
|
|
500
|
+
url = f"{self.base_url}/dispute/{dispute_id}"
|
|
501
|
+
response = await self._client.get(url, headers=self._get_headers())
|
|
502
|
+
response.raise_for_status()
|
|
503
|
+
return Dispute.model_validate(response.json())
|
|
504
|
+
|
|
505
|
+
async def list_escrows(
|
|
506
|
+
self,
|
|
507
|
+
*,
|
|
508
|
+
status: Optional[EscrowStatus] = None,
|
|
509
|
+
payer: Optional[str] = None,
|
|
510
|
+
recipient: Optional[str] = None,
|
|
511
|
+
page: int = 1,
|
|
512
|
+
limit: int = 20,
|
|
513
|
+
) -> EscrowListResponse:
|
|
514
|
+
"""
|
|
515
|
+
List escrow payments with filters.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
status: Filter by status
|
|
519
|
+
payer: Filter by payer address
|
|
520
|
+
recipient: Filter by recipient address
|
|
521
|
+
page: Page number (1-indexed)
|
|
522
|
+
limit: Results per page
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
Paginated list of escrow payments
|
|
526
|
+
|
|
527
|
+
Raises:
|
|
528
|
+
httpx.HTTPStatusError: If the request fails
|
|
529
|
+
"""
|
|
530
|
+
params: dict[str, Any] = {"page": page, "limit": limit}
|
|
531
|
+
if status:
|
|
532
|
+
params["status"] = status.value
|
|
533
|
+
if payer:
|
|
534
|
+
params["payer"] = payer
|
|
535
|
+
if recipient:
|
|
536
|
+
params["recipient"] = recipient
|
|
537
|
+
|
|
538
|
+
url = f"{self.base_url}/escrow"
|
|
539
|
+
response = await self._client.get(
|
|
540
|
+
url,
|
|
541
|
+
params=params,
|
|
542
|
+
headers=self._get_headers(authenticated=True),
|
|
543
|
+
)
|
|
544
|
+
response.raise_for_status()
|
|
545
|
+
return EscrowListResponse.model_validate(response.json())
|
|
546
|
+
|
|
547
|
+
async def health_check(self) -> bool:
|
|
548
|
+
"""
|
|
549
|
+
Check Escrow API health.
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
True if healthy
|
|
553
|
+
"""
|
|
554
|
+
try:
|
|
555
|
+
url = f"{self.base_url}/health"
|
|
556
|
+
response = await self._client.get(url)
|
|
557
|
+
return response.is_success
|
|
558
|
+
except Exception:
|
|
559
|
+
return False
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# Helper functions
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def can_release_escrow(escrow: EscrowPayment) -> bool:
|
|
566
|
+
"""
|
|
567
|
+
Check if an escrow can be released.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
escrow: Escrow payment to check
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
True if the escrow can be released
|
|
574
|
+
"""
|
|
575
|
+
from datetime import datetime
|
|
576
|
+
|
|
577
|
+
if escrow.status != EscrowStatus.HELD:
|
|
578
|
+
return False
|
|
579
|
+
|
|
580
|
+
# Check expiration
|
|
581
|
+
expires_at = datetime.fromisoformat(escrow.expires_at.replace("Z", "+00:00"))
|
|
582
|
+
if expires_at < datetime.now(expires_at.tzinfo):
|
|
583
|
+
return False
|
|
584
|
+
|
|
585
|
+
# Check minimum hold time if specified
|
|
586
|
+
if escrow.release_conditions and escrow.release_conditions.min_hold_time:
|
|
587
|
+
created_at = datetime.fromisoformat(escrow.created_at.replace("Z", "+00:00"))
|
|
588
|
+
min_release_time = created_at.timestamp() + escrow.release_conditions.min_hold_time
|
|
589
|
+
if datetime.now(created_at.tzinfo).timestamp() < min_release_time:
|
|
590
|
+
return False
|
|
591
|
+
|
|
592
|
+
return True
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def can_refund_escrow(escrow: EscrowPayment) -> bool:
|
|
596
|
+
"""
|
|
597
|
+
Check if an escrow can be refunded.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
escrow: Escrow payment to check
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
True if the escrow can be refunded
|
|
604
|
+
"""
|
|
605
|
+
return escrow.status in (EscrowStatus.HELD, EscrowStatus.PENDING)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def is_escrow_expired(escrow: EscrowPayment) -> bool:
|
|
609
|
+
"""
|
|
610
|
+
Check if an escrow is expired.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
escrow: Escrow payment to check
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
True if the escrow is expired
|
|
617
|
+
"""
|
|
618
|
+
from datetime import datetime
|
|
619
|
+
|
|
620
|
+
expires_at = datetime.fromisoformat(escrow.expires_at.replace("Z", "+00:00"))
|
|
621
|
+
return expires_at < datetime.now(expires_at.tzinfo)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def escrow_time_remaining(escrow: EscrowPayment) -> float:
|
|
625
|
+
"""
|
|
626
|
+
Calculate time remaining until escrow expires.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
escrow: Escrow payment to check
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
Seconds until expiration (negative if expired)
|
|
633
|
+
"""
|
|
634
|
+
from datetime import datetime
|
|
635
|
+
|
|
636
|
+
expires_at = datetime.fromisoformat(escrow.expires_at.replace("Z", "+00:00"))
|
|
637
|
+
return (expires_at - datetime.now(expires_at.tzinfo)).total_seconds()
|
|
@@ -4,16 +4,16 @@ Network configurations for x402 payments.
|
|
|
4
4
|
This module provides configuration for all supported blockchain networks,
|
|
5
5
|
including USDC contract addresses, RPC URLs, and network-specific parameters.
|
|
6
6
|
|
|
7
|
-
The SDK supports
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
- 2 SVM
|
|
11
|
-
- 1 NEAR: NEAR Protocol
|
|
12
|
-
- 1 Stellar: Stellar
|
|
13
|
-
-
|
|
14
|
-
-
|
|
7
|
+
The SDK supports 21 blockchain networks across 6 network families:
|
|
8
|
+
- 13 EVM networks: Base, Ethereum, Polygon, Arbitrum, Optimism, Avalanche,
|
|
9
|
+
Celo, HyperEVM, Unichain, Monad, Scroll, SKALE, SKALE Testnet
|
|
10
|
+
- 2 SVM networks: Solana, Fogo
|
|
11
|
+
- 1 NEAR network: NEAR Protocol
|
|
12
|
+
- 1 Stellar network: Stellar
|
|
13
|
+
- 2 Algorand networks: Algorand mainnet, Algorand testnet
|
|
14
|
+
- 2 Sui networks: Sui mainnet, Sui testnet
|
|
15
15
|
|
|
16
|
-
+
|
|
16
|
+
(12 EVM mainnets + 1 EVM testnet, 17 total mainnets)
|
|
17
17
|
|
|
18
18
|
Multi-token support:
|
|
19
19
|
- USDC: All chains
|
uvd_x402_sdk/networks/base.py
CHANGED
|
@@ -395,6 +395,9 @@ _NETWORK_TO_CAIP2 = {
|
|
|
395
395
|
"hyperevm": "eip155:999",
|
|
396
396
|
"unichain": "eip155:130",
|
|
397
397
|
"monad": "eip155:143",
|
|
398
|
+
"scroll": "eip155:534352",
|
|
399
|
+
"skale": "eip155:1187947933",
|
|
400
|
+
"skale-testnet": "eip155:324705682",
|
|
398
401
|
# SVM chains (solana:genesisHash first 32 chars)
|
|
399
402
|
"solana": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
400
403
|
"fogo": "solana:fogo", # Placeholder - update when known
|