uvd-x402-sdk 0.4.1__py3-none-any.whl → 0.4.2__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.
@@ -1,33 +1,51 @@
1
1
  """
2
- Algorand network configurations.
2
+ Algorand network configurations for x402 payments.
3
3
 
4
4
  This module supports Algorand blockchain networks:
5
- - Algorand mainnet
6
- - Algorand testnet
5
+ - Algorand mainnet (network: "algorand-mainnet" or "algorand")
6
+ - Algorand testnet (network: "algorand-testnet")
7
7
 
8
8
  Algorand uses ASA (Algorand Standard Assets) for USDC:
9
9
  - Mainnet USDC ASA ID: 31566704
10
10
  - Testnet USDC ASA ID: 10458941
11
11
 
12
- Payment Flow:
13
- 1. User creates a signed ASA transfer transaction via Pera Wallet
14
- 2. Transaction transfers USDC from user to recipient
15
- 3. Facilitator submits the pre-signed transaction on-chain
16
- 4. User pays ZERO transaction fees (facilitator covers fees)
17
-
18
- Transaction Structure:
19
- - ASA TransferAsset transaction
20
- - Signed by user wallet (Pera Wallet)
21
- - Facilitator submits the signed transaction
12
+ Payment Flow (GoPlausible x402-avm Atomic Group Spec):
13
+ 1. Client creates an ATOMIC GROUP of TWO transactions:
14
+ - Transaction 0 (fee tx): Zero-amount payment FROM facilitator TO facilitator
15
+ This transaction pays fees for both txns. Client creates this UNSIGNED.
16
+ - Transaction 1 (payment tx): ASA transfer FROM client TO merchant
17
+ Client SIGNS this transaction.
18
+ 2. Both transactions share a GROUP ID computed by Algorand SDK.
19
+ 3. Fee pooling: Transaction 0's fee covers Transaction 1's fee (gasless).
20
+ 4. Facilitator completes: Signs transaction 0 and submits the atomic group.
21
+
22
+ Payload Format:
23
+ {
24
+ "x402Version": 1,
25
+ "scheme": "exact",
26
+ "network": "algorand-mainnet",
27
+ "payload": {
28
+ "paymentIndex": 1,
29
+ "paymentGroup": [
30
+ "<base64-msgpack-UNSIGNED-fee-tx>",
31
+ "<base64-msgpack-SIGNED-asa-transfer>"
32
+ ]
33
+ }
34
+ }
22
35
 
23
36
  Address Format:
24
37
  - Algorand addresses are 58 characters, base32 encoded
25
38
  - Example: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ
39
+
40
+ Dependencies:
41
+ - algosdk (optional): Required for building atomic groups
42
+ Install with: pip install py-algorand-sdk
26
43
  """
27
44
 
28
45
  import base64
29
46
  import re
30
- from typing import Any, Dict, Optional
47
+ from dataclasses import dataclass
48
+ from typing import Any, Callable, Dict, List, Optional
31
49
 
32
50
  from uvd_x402_sdk.networks.base import (
33
51
  NetworkConfig,
@@ -63,6 +81,8 @@ ALGORAND = NetworkConfig(
63
81
  "genesis_id": "mainnet-v1.0",
64
82
  # Genesis hash (for CAIP-2)
65
83
  "genesis_hash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
84
+ # x402 network name (facilitator expects this format)
85
+ "x402_network": "algorand-mainnet",
66
86
  },
67
87
  )
68
88
 
@@ -89,6 +109,8 @@ ALGORAND_TESTNET = NetworkConfig(
89
109
  "genesis_id": "testnet-v1.0",
90
110
  # Genesis hash
91
111
  "genesis_hash": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
112
+ # x402 network name (facilitator expects this format)
113
+ "x402_network": "algorand-testnet",
92
114
  },
93
115
  )
94
116
 
@@ -97,6 +119,34 @@ register_network(ALGORAND)
97
119
  register_network(ALGORAND_TESTNET)
98
120
 
99
121
 
122
+ # =============================================================================
123
+ # Algorand Payment Payload (x402-avm Atomic Group Spec)
124
+ # =============================================================================
125
+
126
+
127
+ @dataclass
128
+ class AlgorandPaymentPayload:
129
+ """
130
+ Algorand payment payload for x402 atomic group format.
131
+
132
+ Attributes:
133
+ payment_index: Index of the payment transaction in the group (typically 1)
134
+ payment_group: List of base64-encoded msgpack transactions
135
+ [0] = unsigned fee transaction
136
+ [1] = signed ASA transfer transaction
137
+ """
138
+
139
+ payment_index: int
140
+ payment_group: List[str]
141
+
142
+ def to_dict(self) -> Dict[str, Any]:
143
+ """Convert to dictionary for JSON serialization."""
144
+ return {
145
+ "paymentIndex": self.payment_index,
146
+ "paymentGroup": self.payment_group,
147
+ }
148
+
149
+
100
150
  # =============================================================================
101
151
  # Algorand-specific utilities
102
152
  # =============================================================================
@@ -162,59 +212,99 @@ def is_valid_algorand_address(address: str) -> bool:
162
212
 
163
213
  def validate_algorand_payload(payload: Dict[str, Any]) -> bool:
164
214
  """
165
- Validate an Algorand payment payload structure.
215
+ Validate an Algorand payment payload structure (x402-avm atomic group format).
166
216
 
167
217
  The payload must contain:
168
- - from: Sender's Algorand address
169
- - to: Recipient's Algorand address
170
- - amount: Amount in base units (microUSDC)
171
- - assetId: ASA ID for USDC
172
- - signedTxn: Base64-encoded signed transaction
218
+ - paymentIndex: Index of the payment transaction (typically 1)
219
+ - paymentGroup: List of base64-encoded msgpack transactions
220
+ - [0]: Unsigned fee transaction (facilitator -> facilitator)
221
+ - [1]: Signed ASA transfer (client -> merchant)
173
222
 
174
223
  Args:
175
- payload: Payload dictionary from x402 payment
224
+ payload: Payload dictionary from x402 payment (the inner "payload" field)
176
225
 
177
226
  Returns:
178
227
  True if valid, raises ValueError if invalid
228
+
229
+ Example:
230
+ >>> payload = {
231
+ ... "paymentIndex": 1,
232
+ ... "paymentGroup": [
233
+ ... "base64-unsigned-fee-tx...",
234
+ ... "base64-signed-payment-tx..."
235
+ ... ]
236
+ ... }
237
+ >>> validate_algorand_payload(payload)
238
+ True
179
239
  """
180
- required_fields = ["from", "to", "amount", "assetId", "signedTxn"]
240
+ # Check required fields
241
+ if "paymentIndex" not in payload:
242
+ raise ValueError("Algorand payload missing 'paymentIndex' field")
243
+ if "paymentGroup" not in payload:
244
+ raise ValueError("Algorand payload missing 'paymentGroup' field")
245
+
246
+ # Validate paymentIndex
247
+ payment_index = payload["paymentIndex"]
248
+ if not isinstance(payment_index, int) or payment_index < 0:
249
+ raise ValueError(f"paymentIndex must be a non-negative integer: {payment_index}")
250
+
251
+ # Validate paymentGroup
252
+ payment_group = payload["paymentGroup"]
253
+ if not isinstance(payment_group, list):
254
+ raise ValueError("paymentGroup must be a list")
255
+
256
+ if len(payment_group) < 2:
257
+ raise ValueError(
258
+ f"paymentGroup must contain at least 2 transactions, got {len(payment_group)}"
259
+ )
260
+
261
+ if payment_index >= len(payment_group):
262
+ raise ValueError(
263
+ f"paymentIndex ({payment_index}) out of range for paymentGroup "
264
+ f"(length {len(payment_group)})"
265
+ )
266
+
267
+ # Validate each transaction in the group is valid base64
268
+ for i, txn_b64 in enumerate(payment_group):
269
+ if not isinstance(txn_b64, str):
270
+ raise ValueError(f"paymentGroup[{i}] must be a string")
271
+
272
+ try:
273
+ txn_bytes = base64.b64decode(txn_b64)
274
+ if len(txn_bytes) < 50:
275
+ raise ValueError(
276
+ f"paymentGroup[{i}] transaction too short: {len(txn_bytes)} bytes"
277
+ )
278
+ except Exception as e:
279
+ raise ValueError(
280
+ f"paymentGroup[{i}] is not valid base64: {e}"
281
+ ) from e
181
282
 
182
- for field in required_fields:
183
- if field not in payload:
184
- raise ValueError(f"Algorand payload missing '{field}' field")
283
+ return True
185
284
 
186
- # Validate addresses
187
- if not is_valid_algorand_address(payload["from"]):
188
- raise ValueError(f"Invalid 'from' address: {payload['from']}")
189
- if not is_valid_algorand_address(payload["to"]):
190
- raise ValueError(f"Invalid 'to' address: {payload['to']}")
191
285
 
192
- # Validate amount
193
- try:
194
- amount = int(payload["amount"])
195
- if amount <= 0:
196
- raise ValueError(f"Amount must be positive: {amount}")
197
- except (ValueError, TypeError) as e:
198
- raise ValueError(f"Invalid amount: {payload['amount']}") from e
286
+ def get_x402_network_name(network_name: str) -> str:
287
+ """
288
+ Get the x402 network name for an Algorand network.
199
289
 
200
- # Validate assetId
201
- try:
202
- asset_id = int(payload["assetId"])
203
- if asset_id <= 0:
204
- raise ValueError(f"Asset ID must be positive: {asset_id}")
205
- except (ValueError, TypeError) as e:
206
- raise ValueError(f"Invalid assetId: {payload['assetId']}") from e
290
+ The facilitator expects "algorand-mainnet" or "algorand-testnet".
207
291
 
208
- # Validate signedTxn is valid base64
209
- try:
210
- signed_txn = payload["signedTxn"]
211
- tx_bytes = base64.b64decode(signed_txn)
212
- if len(tx_bytes) < 50:
213
- raise ValueError(f"Signed transaction too short: {len(tx_bytes)} bytes")
214
- except Exception as e:
215
- raise ValueError(f"Invalid signedTxn (not valid base64): {e}") from e
292
+ Args:
293
+ network_name: SDK network name ('algorand' or 'algorand-testnet')
216
294
 
217
- return True
295
+ Returns:
296
+ x402 network name ('algorand-mainnet' or 'algorand-testnet')
297
+ """
298
+ from uvd_x402_sdk.networks.base import get_network
299
+
300
+ network = get_network(network_name)
301
+ if not network:
302
+ # Default mapping
303
+ if network_name == "algorand":
304
+ return "algorand-mainnet"
305
+ return network_name
306
+
307
+ return network.extra_config.get("x402_network", network_name)
218
308
 
219
309
 
220
310
  def get_explorer_tx_url(network_name: str, tx_id: str) -> Optional[str]:
@@ -285,3 +375,176 @@ def get_usdc_asa_id(network_name: str) -> Optional[int]:
285
375
  return int(network.usdc_address)
286
376
  except (ValueError, TypeError):
287
377
  return None
378
+
379
+
380
+ # =============================================================================
381
+ # Atomic Group Builder (requires algosdk)
382
+ # =============================================================================
383
+
384
+
385
+ def build_atomic_group(
386
+ sender_address: str,
387
+ recipient_address: str,
388
+ amount: int,
389
+ asset_id: int,
390
+ facilitator_address: str,
391
+ sign_transaction: Callable,
392
+ algod_client: Optional[Any] = None,
393
+ suggested_params: Optional[Any] = None,
394
+ ) -> AlgorandPaymentPayload:
395
+ """
396
+ Build an Algorand atomic group for x402 payment.
397
+
398
+ This creates the two-transaction atomic group required by the facilitator:
399
+ - Transaction 0: Unsigned fee payment (facilitator -> facilitator, 0 amount)
400
+ - Transaction 1: Signed ASA transfer (sender -> recipient)
401
+
402
+ Requires: pip install py-algorand-sdk
403
+
404
+ Args:
405
+ sender_address: Client's Algorand address
406
+ recipient_address: Merchant's Algorand address (from payTo)
407
+ amount: Amount in micro-units (1 USDC = 1,000,000)
408
+ asset_id: USDC ASA ID (31566704 mainnet, 10458941 testnet)
409
+ facilitator_address: Facilitator address (from extra.feePayer)
410
+ sign_transaction: Function that signs a transaction.
411
+ Signature: (transaction) -> SignedTransaction
412
+ Can use algosdk's transaction.sign(private_key)
413
+ algod_client: Optional AlgodClient for getting suggested params.
414
+ If not provided, suggested_params must be given.
415
+ suggested_params: Optional SuggestedParams. If not provided,
416
+ algod_client.suggested_params() is called.
417
+
418
+ Returns:
419
+ AlgorandPaymentPayload with paymentIndex and paymentGroup
420
+
421
+ Raises:
422
+ ImportError: If algosdk is not installed
423
+ ValueError: If neither algod_client nor suggested_params provided
424
+
425
+ Example:
426
+ >>> from algosdk import transaction
427
+ >>> from algosdk.v2client import algod
428
+ >>>
429
+ >>> client = algod.AlgodClient("", "https://mainnet-api.algonode.cloud")
430
+ >>> payload = build_atomic_group(
431
+ ... sender_address="SENDER...",
432
+ ... recipient_address="MERCHANT...",
433
+ ... amount=1000000, # 1 USDC
434
+ ... asset_id=31566704,
435
+ ... facilitator_address="FACILITATOR...",
436
+ ... sign_transaction=lambda txn: txn.sign(private_key),
437
+ ... algod_client=client,
438
+ ... )
439
+ """
440
+ try:
441
+ from algosdk import encoding, transaction
442
+ except ImportError as e:
443
+ raise ImportError(
444
+ "algosdk is required for building atomic groups. "
445
+ "Install with: pip install py-algorand-sdk"
446
+ ) from e
447
+
448
+ # Get suggested params
449
+ if suggested_params is None:
450
+ if algod_client is None:
451
+ raise ValueError(
452
+ "Either algod_client or suggested_params must be provided"
453
+ )
454
+ suggested_params = algod_client.suggested_params()
455
+
456
+ # Transaction 0: Fee payment (facilitator -> facilitator, 0 amount)
457
+ # This transaction pays fees for both txns in the group
458
+ fee_txn = transaction.PaymentTxn(
459
+ sender=facilitator_address,
460
+ receiver=facilitator_address, # self-transfer
461
+ amt=0,
462
+ sp=suggested_params,
463
+ )
464
+ # Cover both transactions (1000 microAlgos each = 2000 total)
465
+ fee_txn.fee = 2000
466
+
467
+ # Transaction 1: ASA transfer (client -> merchant)
468
+ payment_txn = transaction.AssetTransferTxn(
469
+ sender=sender_address,
470
+ receiver=recipient_address,
471
+ amt=amount,
472
+ index=asset_id,
473
+ sp=suggested_params,
474
+ )
475
+ # Fee paid by transaction 0
476
+ payment_txn.fee = 0
477
+
478
+ # Assign group ID to both transactions
479
+ group_id = transaction.calculate_group_id([fee_txn, payment_txn])
480
+ fee_txn.group = group_id
481
+ payment_txn.group = group_id
482
+
483
+ # Encode fee transaction (UNSIGNED - facilitator will sign)
484
+ unsigned_fee_txn_bytes = encoding.msgpack_encode(fee_txn)
485
+ unsigned_fee_txn_base64 = base64.b64encode(unsigned_fee_txn_bytes).decode("utf-8")
486
+
487
+ # Sign and encode payment transaction
488
+ signed_payment_txn = sign_transaction(payment_txn)
489
+ signed_payment_txn_bytes = encoding.msgpack_encode(signed_payment_txn)
490
+ signed_payment_txn_base64 = base64.b64encode(signed_payment_txn_bytes).decode("utf-8")
491
+
492
+ return AlgorandPaymentPayload(
493
+ payment_index=1, # Index of the payment transaction
494
+ payment_group=[
495
+ unsigned_fee_txn_base64, # Transaction 0: unsigned fee tx
496
+ signed_payment_txn_base64, # Transaction 1: signed payment tx
497
+ ],
498
+ )
499
+
500
+
501
+ def create_private_key_signer(private_key: str) -> Callable:
502
+ """
503
+ Create a transaction signer from a private key.
504
+
505
+ Args:
506
+ private_key: Algorand private key (base64 encoded)
507
+
508
+ Returns:
509
+ Function that signs transactions
510
+
511
+ Example:
512
+ >>> signer = create_private_key_signer(my_private_key)
513
+ >>> payload = build_atomic_group(..., sign_transaction=signer)
514
+ """
515
+ def sign(txn: Any) -> Any:
516
+ return txn.sign(private_key)
517
+ return sign
518
+
519
+
520
+ def build_x402_payment_request(
521
+ payload: AlgorandPaymentPayload,
522
+ network: str = "algorand-mainnet",
523
+ scheme: str = "exact",
524
+ version: int = 1,
525
+ ) -> Dict[str, Any]:
526
+ """
527
+ Build a complete x402 payment request for Algorand.
528
+
529
+ Args:
530
+ payload: AlgorandPaymentPayload from build_atomic_group()
531
+ network: Network name ("algorand-mainnet" or "algorand-testnet")
532
+ scheme: Payment scheme (default "exact")
533
+ version: x402 version (default 1)
534
+
535
+ Returns:
536
+ Complete x402 payment request dictionary
537
+
538
+ Example:
539
+ >>> payload = build_atomic_group(...)
540
+ >>> request = build_x402_payment_request(payload)
541
+ >>> # Send as X-PAYMENT header (base64 encoded JSON)
542
+ >>> import json, base64
543
+ >>> header = base64.b64encode(json.dumps(request).encode()).decode()
544
+ """
545
+ return {
546
+ "x402Version": version,
547
+ "scheme": scheme,
548
+ "network": network,
549
+ "payload": payload.to_dict(),
550
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uvd-x402-sdk
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Python SDK for x402 payments - gasless crypto payments across 16 blockchains with multi-stablecoin support (USDC, EURC, AUSD, PYUSD, USDT)
5
5
  Author-email: Ultravioleta DAO <dev@ultravioletadao.xyz>
6
6
  Project-URL: Homepage, https://github.com/UltravioletaDAO/uvd-x402-sdk-python
@@ -24,8 +24,10 @@ Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: httpx>=0.24.0
26
26
  Requires-Dist: pydantic>=2.0.0
27
+ Provides-Extra: algorand
28
+ Requires-Dist: py-algorand-sdk>=2.0.0; extra == "algorand"
27
29
  Provides-Extra: all
28
- Requires-Dist: uvd-x402-sdk[aws,django,fastapi,flask,web3]; extra == "all"
30
+ Requires-Dist: uvd-x402-sdk[algorand,aws,django,fastapi,flask,web3]; extra == "all"
29
31
  Provides-Extra: aws
30
32
  Requires-Dist: boto3>=1.26.0; extra == "aws"
31
33
  Provides-Extra: dev
@@ -11,14 +11,14 @@ uvd_x402_sdk/integrations/fastapi_integration.py,sha256=j5h1IJwFLBBoWov7ANLCFaxe
11
11
  uvd_x402_sdk/integrations/flask_integration.py,sha256=0iQKO5-WRxE76Pv-1jEl4lYhjCLmq_R-jxR5g9xIcKw,8825
12
12
  uvd_x402_sdk/integrations/lambda_integration.py,sha256=nRf4o3nS6Syx-d5P0kEhz66y7jb_S4w-mwaIazgiA9c,10184
13
13
  uvd_x402_sdk/networks/__init__.py,sha256=LKl_TljVoCDb27YB4X_VbQN8XKbdwWFAsCwgiqQtlgo,2092
14
- uvd_x402_sdk/networks/algorand.py,sha256=ACaVMyXItRX3M29GPwwMMxECMjNXx3FlYARNBeAztH4,8601
14
+ uvd_x402_sdk/networks/algorand.py,sha256=zcpU5U4V9weiFZ_Zfc-khWaadTldO6KKOIg5ZdxXaRs,17819
15
15
  uvd_x402_sdk/networks/base.py,sha256=gOPWfqasGbgtg9w2uG5pWnfjdOEain92L2egnDSBguc,14863
16
16
  uvd_x402_sdk/networks/evm.py,sha256=4IbeaMH2I1c9DYCijghys0qYNeL2Nl92IMKLwq-b0Zg,10065
17
17
  uvd_x402_sdk/networks/near.py,sha256=sxbxT1NqjcENh8ysFLDpAx5DGizf1EI0YjwgviLfqcY,11608
18
18
  uvd_x402_sdk/networks/solana.py,sha256=-snAeE3OxJU_kaGb_z4NOU6k0SGAD4DBPhJcPbgrdgo,11675
19
19
  uvd_x402_sdk/networks/stellar.py,sha256=c-6re-dVc2-6gJ5rL4krUTaFsPz5vkactOJD-0wowBA,3534
20
- uvd_x402_sdk-0.4.1.dist-info/LICENSE,sha256=OcLzB_iSgMbvk7b0dlyvleY_IbL2WUaPxvn1CHw2uAc,1073
21
- uvd_x402_sdk-0.4.1.dist-info/METADATA,sha256=3cD7Kz9_fONGwbZVUBMOxXqglRmC_wikuBH8OWpIT24,29618
22
- uvd_x402_sdk-0.4.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
23
- uvd_x402_sdk-0.4.1.dist-info/top_level.txt,sha256=Exyjj_Kl7CDAGFMi72lT9oFPOYiRNZb3l8tr906mMmc,13
24
- uvd_x402_sdk-0.4.1.dist-info/RECORD,,
20
+ uvd_x402_sdk-0.4.2.dist-info/LICENSE,sha256=OcLzB_iSgMbvk7b0dlyvleY_IbL2WUaPxvn1CHw2uAc,1073
21
+ uvd_x402_sdk-0.4.2.dist-info/METADATA,sha256=dOuyD4J4cWQtHx3Ia0uhst7xIf4BMwhEr3ULgPkbljw,29711
22
+ uvd_x402_sdk-0.4.2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
23
+ uvd_x402_sdk-0.4.2.dist-info/top_level.txt,sha256=Exyjj_Kl7CDAGFMi72lT9oFPOYiRNZb3l8tr906mMmc,13
24
+ uvd_x402_sdk-0.4.2.dist-info/RECORD,,