uvd-x402-sdk 0.4.1__tar.gz → 0.4.2__tar.gz
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-0.4.1/src/uvd_x402_sdk.egg-info → uvd_x402_sdk-0.4.2}/PKG-INFO +4 -2
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/pyproject.toml +5 -2
- uvd_x402_sdk-0.4.2/src/uvd_x402_sdk/networks/algorand.py +550 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2/src/uvd_x402_sdk.egg-info}/PKG-INFO +4 -2
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk.egg-info/requires.txt +4 -1
- uvd_x402_sdk-0.4.1/src/uvd_x402_sdk/networks/algorand.py +0 -287
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/LICENSE +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/README.md +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/setup.cfg +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/__init__.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/client.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/config.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/decorators.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/exceptions.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/integrations/__init__.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/integrations/django_integration.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/integrations/fastapi_integration.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/integrations/flask_integration.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/integrations/lambda_integration.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/models.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/networks/__init__.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/networks/base.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/networks/evm.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/networks/near.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/networks/solana.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/networks/stellar.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/response.py +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk.egg-info/SOURCES.txt +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk.egg-info/dependency_links.txt +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk.egg-info/top_level.txt +0 -0
- {uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: uvd-x402-sdk
|
|
3
|
-
Version: 0.4.
|
|
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
|
|
@@ -36,8 +36,10 @@ Provides-Extra: django
|
|
|
36
36
|
Requires-Dist: django>=4.0.0; extra == "django"
|
|
37
37
|
Provides-Extra: aws
|
|
38
38
|
Requires-Dist: boto3>=1.26.0; extra == "aws"
|
|
39
|
+
Provides-Extra: algorand
|
|
40
|
+
Requires-Dist: py-algorand-sdk>=2.0.0; extra == "algorand"
|
|
39
41
|
Provides-Extra: all
|
|
40
|
-
Requires-Dist: uvd-x402-sdk[aws,django,fastapi,flask,web3]; extra == "all"
|
|
42
|
+
Requires-Dist: uvd-x402-sdk[algorand,aws,django,fastapi,flask,web3]; extra == "all"
|
|
41
43
|
Provides-Extra: dev
|
|
42
44
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
43
45
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "uvd-x402-sdk"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Python SDK for x402 payments - gasless crypto payments across 16 blockchains with multi-stablecoin support (USDC, EURC, AUSD, PYUSD, USDT)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -65,8 +65,11 @@ django = [
|
|
|
65
65
|
aws = [
|
|
66
66
|
"boto3>=1.26.0",
|
|
67
67
|
]
|
|
68
|
+
algorand = [
|
|
69
|
+
"py-algorand-sdk>=2.0.0",
|
|
70
|
+
]
|
|
68
71
|
all = [
|
|
69
|
-
"uvd-x402-sdk[web3,flask,fastapi,django,aws]",
|
|
72
|
+
"uvd-x402-sdk[web3,flask,fastapi,django,aws,algorand]",
|
|
70
73
|
]
|
|
71
74
|
dev = [
|
|
72
75
|
"pytest>=7.0.0",
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Algorand network configurations for x402 payments.
|
|
3
|
+
|
|
4
|
+
This module supports Algorand blockchain networks:
|
|
5
|
+
- Algorand mainnet (network: "algorand-mainnet" or "algorand")
|
|
6
|
+
- Algorand testnet (network: "algorand-testnet")
|
|
7
|
+
|
|
8
|
+
Algorand uses ASA (Algorand Standard Assets) for USDC:
|
|
9
|
+
- Mainnet USDC ASA ID: 31566704
|
|
10
|
+
- Testnet USDC ASA ID: 10458941
|
|
11
|
+
|
|
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
|
+
}
|
|
35
|
+
|
|
36
|
+
Address Format:
|
|
37
|
+
- Algorand addresses are 58 characters, base32 encoded
|
|
38
|
+
- Example: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ
|
|
39
|
+
|
|
40
|
+
Dependencies:
|
|
41
|
+
- algosdk (optional): Required for building atomic groups
|
|
42
|
+
Install with: pip install py-algorand-sdk
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import base64
|
|
46
|
+
import re
|
|
47
|
+
from dataclasses import dataclass
|
|
48
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
49
|
+
|
|
50
|
+
from uvd_x402_sdk.networks.base import (
|
|
51
|
+
NetworkConfig,
|
|
52
|
+
NetworkType,
|
|
53
|
+
register_network,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# Algorand Networks Configuration
|
|
59
|
+
# =============================================================================
|
|
60
|
+
|
|
61
|
+
# Algorand Mainnet
|
|
62
|
+
ALGORAND = NetworkConfig(
|
|
63
|
+
name="algorand",
|
|
64
|
+
display_name="Algorand",
|
|
65
|
+
network_type=NetworkType.ALGORAND,
|
|
66
|
+
chain_id=0, # Non-EVM, no chain ID
|
|
67
|
+
usdc_address="31566704", # USDC ASA ID on mainnet
|
|
68
|
+
usdc_decimals=6,
|
|
69
|
+
usdc_domain_name="", # Not applicable for Algorand
|
|
70
|
+
usdc_domain_version="",
|
|
71
|
+
rpc_url="https://mainnet-api.algonode.cloud",
|
|
72
|
+
enabled=True,
|
|
73
|
+
extra_config={
|
|
74
|
+
# ASA (Algorand Standard Asset) details
|
|
75
|
+
"usdc_asa_id": 31566704,
|
|
76
|
+
# Block explorer
|
|
77
|
+
"explorer_url": "https://allo.info",
|
|
78
|
+
# Indexer endpoint (for account queries)
|
|
79
|
+
"indexer_url": "https://mainnet-idx.algonode.cloud",
|
|
80
|
+
# Network identifier
|
|
81
|
+
"genesis_id": "mainnet-v1.0",
|
|
82
|
+
# Genesis hash (for CAIP-2)
|
|
83
|
+
"genesis_hash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
|
|
84
|
+
# x402 network name (facilitator expects this format)
|
|
85
|
+
"x402_network": "algorand-mainnet",
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Algorand Testnet
|
|
90
|
+
ALGORAND_TESTNET = NetworkConfig(
|
|
91
|
+
name="algorand-testnet",
|
|
92
|
+
display_name="Algorand Testnet",
|
|
93
|
+
network_type=NetworkType.ALGORAND,
|
|
94
|
+
chain_id=0, # Non-EVM, no chain ID
|
|
95
|
+
usdc_address="10458941", # USDC ASA ID on testnet
|
|
96
|
+
usdc_decimals=6,
|
|
97
|
+
usdc_domain_name="", # Not applicable for Algorand
|
|
98
|
+
usdc_domain_version="",
|
|
99
|
+
rpc_url="https://testnet-api.algonode.cloud",
|
|
100
|
+
enabled=True,
|
|
101
|
+
extra_config={
|
|
102
|
+
# ASA (Algorand Standard Asset) details
|
|
103
|
+
"usdc_asa_id": 10458941,
|
|
104
|
+
# Block explorer
|
|
105
|
+
"explorer_url": "https://testnet.allo.info",
|
|
106
|
+
# Indexer endpoint (for account queries)
|
|
107
|
+
"indexer_url": "https://testnet-idx.algonode.cloud",
|
|
108
|
+
# Network identifier
|
|
109
|
+
"genesis_id": "testnet-v1.0",
|
|
110
|
+
# Genesis hash
|
|
111
|
+
"genesis_hash": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
|
|
112
|
+
# x402 network name (facilitator expects this format)
|
|
113
|
+
"x402_network": "algorand-testnet",
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Register Algorand networks
|
|
118
|
+
register_network(ALGORAND)
|
|
119
|
+
register_network(ALGORAND_TESTNET)
|
|
120
|
+
|
|
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
|
+
|
|
150
|
+
# =============================================================================
|
|
151
|
+
# Algorand-specific utilities
|
|
152
|
+
# =============================================================================
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def is_algorand_network(network_name: str) -> bool:
|
|
156
|
+
"""
|
|
157
|
+
Check if a network is Algorand.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
network_name: Network name to check
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if network is Algorand (mainnet or testnet)
|
|
164
|
+
"""
|
|
165
|
+
from uvd_x402_sdk.networks.base import get_network, NetworkType
|
|
166
|
+
|
|
167
|
+
network = get_network(network_name)
|
|
168
|
+
if not network:
|
|
169
|
+
return False
|
|
170
|
+
return network.network_type == NetworkType.ALGORAND
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_algorand_networks() -> list:
|
|
174
|
+
"""
|
|
175
|
+
Get all registered Algorand networks.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
List of Algorand NetworkConfig instances
|
|
179
|
+
"""
|
|
180
|
+
from uvd_x402_sdk.networks.base import list_networks, NetworkType
|
|
181
|
+
|
|
182
|
+
return [
|
|
183
|
+
n for n in list_networks(enabled_only=True)
|
|
184
|
+
if n.network_type == NetworkType.ALGORAND
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def is_valid_algorand_address(address: str) -> bool:
|
|
189
|
+
"""
|
|
190
|
+
Validate an Algorand address format.
|
|
191
|
+
|
|
192
|
+
Algorand addresses are 58 characters, base32 encoded (RFC 4648).
|
|
193
|
+
They consist of uppercase letters A-Z and digits 2-7.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
address: Address to validate
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
True if valid Algorand address format
|
|
200
|
+
"""
|
|
201
|
+
if not address or not isinstance(address, str):
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
# Algorand addresses are exactly 58 characters
|
|
205
|
+
if len(address) != 58:
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
# Base32 alphabet (RFC 4648): A-Z and 2-7
|
|
209
|
+
base32_pattern = re.compile(r'^[A-Z2-7]+$')
|
|
210
|
+
return bool(base32_pattern.match(address))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def validate_algorand_payload(payload: Dict[str, Any]) -> bool:
|
|
214
|
+
"""
|
|
215
|
+
Validate an Algorand payment payload structure (x402-avm atomic group format).
|
|
216
|
+
|
|
217
|
+
The payload must contain:
|
|
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)
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
payload: Payload dictionary from x402 payment (the inner "payload" field)
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
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
|
|
239
|
+
"""
|
|
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
|
|
282
|
+
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def get_x402_network_name(network_name: str) -> str:
|
|
287
|
+
"""
|
|
288
|
+
Get the x402 network name for an Algorand network.
|
|
289
|
+
|
|
290
|
+
The facilitator expects "algorand-mainnet" or "algorand-testnet".
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
network_name: SDK network name ('algorand' or 'algorand-testnet')
|
|
294
|
+
|
|
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)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def get_explorer_tx_url(network_name: str, tx_id: str) -> Optional[str]:
|
|
311
|
+
"""
|
|
312
|
+
Get block explorer URL for a transaction.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
network_name: Network name ('algorand' or 'algorand-testnet')
|
|
316
|
+
tx_id: Transaction ID
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Explorer URL or None if network not found
|
|
320
|
+
"""
|
|
321
|
+
from uvd_x402_sdk.networks.base import get_network
|
|
322
|
+
|
|
323
|
+
network = get_network(network_name)
|
|
324
|
+
if not network or network.network_type != NetworkType.ALGORAND:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
explorer_url = network.extra_config.get("explorer_url", "https://allo.info")
|
|
328
|
+
return f"{explorer_url}/tx/{tx_id}"
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def get_explorer_address_url(network_name: str, address: str) -> Optional[str]:
|
|
332
|
+
"""
|
|
333
|
+
Get block explorer URL for an address.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
network_name: Network name ('algorand' or 'algorand-testnet')
|
|
337
|
+
address: Algorand address
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Explorer URL or None if network not found
|
|
341
|
+
"""
|
|
342
|
+
from uvd_x402_sdk.networks.base import get_network
|
|
343
|
+
|
|
344
|
+
network = get_network(network_name)
|
|
345
|
+
if not network or network.network_type != NetworkType.ALGORAND:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
explorer_url = network.extra_config.get("explorer_url", "https://allo.info")
|
|
349
|
+
return f"{explorer_url}/account/{address}"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def get_usdc_asa_id(network_name: str) -> Optional[int]:
|
|
353
|
+
"""
|
|
354
|
+
Get the USDC ASA ID for an Algorand network.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
network_name: Network name ('algorand' or 'algorand-testnet')
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
USDC ASA ID or None if network not found
|
|
361
|
+
"""
|
|
362
|
+
from uvd_x402_sdk.networks.base import get_network
|
|
363
|
+
|
|
364
|
+
network = get_network(network_name)
|
|
365
|
+
if not network or network.network_type != NetworkType.ALGORAND:
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
# Try extra_config first, then fall back to usdc_address
|
|
369
|
+
asa_id = network.extra_config.get("usdc_asa_id")
|
|
370
|
+
if asa_id:
|
|
371
|
+
return int(asa_id)
|
|
372
|
+
|
|
373
|
+
# Parse from usdc_address (which stores the ASA ID as string)
|
|
374
|
+
try:
|
|
375
|
+
return int(network.usdc_address)
|
|
376
|
+
except (ValueError, TypeError):
|
|
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.
|
|
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
|
|
@@ -36,8 +36,10 @@ Provides-Extra: django
|
|
|
36
36
|
Requires-Dist: django>=4.0.0; extra == "django"
|
|
37
37
|
Provides-Extra: aws
|
|
38
38
|
Requires-Dist: boto3>=1.26.0; extra == "aws"
|
|
39
|
+
Provides-Extra: algorand
|
|
40
|
+
Requires-Dist: py-algorand-sdk>=2.0.0; extra == "algorand"
|
|
39
41
|
Provides-Extra: all
|
|
40
|
-
Requires-Dist: uvd-x402-sdk[aws,django,fastapi,flask,web3]; extra == "all"
|
|
42
|
+
Requires-Dist: uvd-x402-sdk[algorand,aws,django,fastapi,flask,web3]; extra == "all"
|
|
41
43
|
Provides-Extra: dev
|
|
42
44
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
43
45
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Algorand network configurations.
|
|
3
|
-
|
|
4
|
-
This module supports Algorand blockchain networks:
|
|
5
|
-
- Algorand mainnet
|
|
6
|
-
- Algorand testnet
|
|
7
|
-
|
|
8
|
-
Algorand uses ASA (Algorand Standard Assets) for USDC:
|
|
9
|
-
- Mainnet USDC ASA ID: 31566704
|
|
10
|
-
- Testnet USDC ASA ID: 10458941
|
|
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
|
|
22
|
-
|
|
23
|
-
Address Format:
|
|
24
|
-
- Algorand addresses are 58 characters, base32 encoded
|
|
25
|
-
- Example: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
import base64
|
|
29
|
-
import re
|
|
30
|
-
from typing import Any, Dict, Optional
|
|
31
|
-
|
|
32
|
-
from uvd_x402_sdk.networks.base import (
|
|
33
|
-
NetworkConfig,
|
|
34
|
-
NetworkType,
|
|
35
|
-
register_network,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# =============================================================================
|
|
40
|
-
# Algorand Networks Configuration
|
|
41
|
-
# =============================================================================
|
|
42
|
-
|
|
43
|
-
# Algorand Mainnet
|
|
44
|
-
ALGORAND = NetworkConfig(
|
|
45
|
-
name="algorand",
|
|
46
|
-
display_name="Algorand",
|
|
47
|
-
network_type=NetworkType.ALGORAND,
|
|
48
|
-
chain_id=0, # Non-EVM, no chain ID
|
|
49
|
-
usdc_address="31566704", # USDC ASA ID on mainnet
|
|
50
|
-
usdc_decimals=6,
|
|
51
|
-
usdc_domain_name="", # Not applicable for Algorand
|
|
52
|
-
usdc_domain_version="",
|
|
53
|
-
rpc_url="https://mainnet-api.algonode.cloud",
|
|
54
|
-
enabled=True,
|
|
55
|
-
extra_config={
|
|
56
|
-
# ASA (Algorand Standard Asset) details
|
|
57
|
-
"usdc_asa_id": 31566704,
|
|
58
|
-
# Block explorer
|
|
59
|
-
"explorer_url": "https://allo.info",
|
|
60
|
-
# Indexer endpoint (for account queries)
|
|
61
|
-
"indexer_url": "https://mainnet-idx.algonode.cloud",
|
|
62
|
-
# Network identifier
|
|
63
|
-
"genesis_id": "mainnet-v1.0",
|
|
64
|
-
# Genesis hash (for CAIP-2)
|
|
65
|
-
"genesis_hash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
|
|
66
|
-
},
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
# Algorand Testnet
|
|
70
|
-
ALGORAND_TESTNET = NetworkConfig(
|
|
71
|
-
name="algorand-testnet",
|
|
72
|
-
display_name="Algorand Testnet",
|
|
73
|
-
network_type=NetworkType.ALGORAND,
|
|
74
|
-
chain_id=0, # Non-EVM, no chain ID
|
|
75
|
-
usdc_address="10458941", # USDC ASA ID on testnet
|
|
76
|
-
usdc_decimals=6,
|
|
77
|
-
usdc_domain_name="", # Not applicable for Algorand
|
|
78
|
-
usdc_domain_version="",
|
|
79
|
-
rpc_url="https://testnet-api.algonode.cloud",
|
|
80
|
-
enabled=True,
|
|
81
|
-
extra_config={
|
|
82
|
-
# ASA (Algorand Standard Asset) details
|
|
83
|
-
"usdc_asa_id": 10458941,
|
|
84
|
-
# Block explorer
|
|
85
|
-
"explorer_url": "https://testnet.allo.info",
|
|
86
|
-
# Indexer endpoint (for account queries)
|
|
87
|
-
"indexer_url": "https://testnet-idx.algonode.cloud",
|
|
88
|
-
# Network identifier
|
|
89
|
-
"genesis_id": "testnet-v1.0",
|
|
90
|
-
# Genesis hash
|
|
91
|
-
"genesis_hash": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
|
|
92
|
-
},
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
# Register Algorand networks
|
|
96
|
-
register_network(ALGORAND)
|
|
97
|
-
register_network(ALGORAND_TESTNET)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# =============================================================================
|
|
101
|
-
# Algorand-specific utilities
|
|
102
|
-
# =============================================================================
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def is_algorand_network(network_name: str) -> bool:
|
|
106
|
-
"""
|
|
107
|
-
Check if a network is Algorand.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
network_name: Network name to check
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
True if network is Algorand (mainnet or testnet)
|
|
114
|
-
"""
|
|
115
|
-
from uvd_x402_sdk.networks.base import get_network, NetworkType
|
|
116
|
-
|
|
117
|
-
network = get_network(network_name)
|
|
118
|
-
if not network:
|
|
119
|
-
return False
|
|
120
|
-
return network.network_type == NetworkType.ALGORAND
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def get_algorand_networks() -> list:
|
|
124
|
-
"""
|
|
125
|
-
Get all registered Algorand networks.
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
List of Algorand NetworkConfig instances
|
|
129
|
-
"""
|
|
130
|
-
from uvd_x402_sdk.networks.base import list_networks, NetworkType
|
|
131
|
-
|
|
132
|
-
return [
|
|
133
|
-
n for n in list_networks(enabled_only=True)
|
|
134
|
-
if n.network_type == NetworkType.ALGORAND
|
|
135
|
-
]
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def is_valid_algorand_address(address: str) -> bool:
|
|
139
|
-
"""
|
|
140
|
-
Validate an Algorand address format.
|
|
141
|
-
|
|
142
|
-
Algorand addresses are 58 characters, base32 encoded (RFC 4648).
|
|
143
|
-
They consist of uppercase letters A-Z and digits 2-7.
|
|
144
|
-
|
|
145
|
-
Args:
|
|
146
|
-
address: Address to validate
|
|
147
|
-
|
|
148
|
-
Returns:
|
|
149
|
-
True if valid Algorand address format
|
|
150
|
-
"""
|
|
151
|
-
if not address or not isinstance(address, str):
|
|
152
|
-
return False
|
|
153
|
-
|
|
154
|
-
# Algorand addresses are exactly 58 characters
|
|
155
|
-
if len(address) != 58:
|
|
156
|
-
return False
|
|
157
|
-
|
|
158
|
-
# Base32 alphabet (RFC 4648): A-Z and 2-7
|
|
159
|
-
base32_pattern = re.compile(r'^[A-Z2-7]+$')
|
|
160
|
-
return bool(base32_pattern.match(address))
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def validate_algorand_payload(payload: Dict[str, Any]) -> bool:
|
|
164
|
-
"""
|
|
165
|
-
Validate an Algorand payment payload structure.
|
|
166
|
-
|
|
167
|
-
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
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
payload: Payload dictionary from x402 payment
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
True if valid, raises ValueError if invalid
|
|
179
|
-
"""
|
|
180
|
-
required_fields = ["from", "to", "amount", "assetId", "signedTxn"]
|
|
181
|
-
|
|
182
|
-
for field in required_fields:
|
|
183
|
-
if field not in payload:
|
|
184
|
-
raise ValueError(f"Algorand payload missing '{field}' field")
|
|
185
|
-
|
|
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
|
-
|
|
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
|
|
199
|
-
|
|
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
|
|
207
|
-
|
|
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
|
|
216
|
-
|
|
217
|
-
return True
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def get_explorer_tx_url(network_name: str, tx_id: str) -> Optional[str]:
|
|
221
|
-
"""
|
|
222
|
-
Get block explorer URL for a transaction.
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
network_name: Network name ('algorand' or 'algorand-testnet')
|
|
226
|
-
tx_id: Transaction ID
|
|
227
|
-
|
|
228
|
-
Returns:
|
|
229
|
-
Explorer URL or None if network not found
|
|
230
|
-
"""
|
|
231
|
-
from uvd_x402_sdk.networks.base import get_network
|
|
232
|
-
|
|
233
|
-
network = get_network(network_name)
|
|
234
|
-
if not network or network.network_type != NetworkType.ALGORAND:
|
|
235
|
-
return None
|
|
236
|
-
|
|
237
|
-
explorer_url = network.extra_config.get("explorer_url", "https://allo.info")
|
|
238
|
-
return f"{explorer_url}/tx/{tx_id}"
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def get_explorer_address_url(network_name: str, address: str) -> Optional[str]:
|
|
242
|
-
"""
|
|
243
|
-
Get block explorer URL for an address.
|
|
244
|
-
|
|
245
|
-
Args:
|
|
246
|
-
network_name: Network name ('algorand' or 'algorand-testnet')
|
|
247
|
-
address: Algorand address
|
|
248
|
-
|
|
249
|
-
Returns:
|
|
250
|
-
Explorer URL or None if network not found
|
|
251
|
-
"""
|
|
252
|
-
from uvd_x402_sdk.networks.base import get_network
|
|
253
|
-
|
|
254
|
-
network = get_network(network_name)
|
|
255
|
-
if not network or network.network_type != NetworkType.ALGORAND:
|
|
256
|
-
return None
|
|
257
|
-
|
|
258
|
-
explorer_url = network.extra_config.get("explorer_url", "https://allo.info")
|
|
259
|
-
return f"{explorer_url}/account/{address}"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def get_usdc_asa_id(network_name: str) -> Optional[int]:
|
|
263
|
-
"""
|
|
264
|
-
Get the USDC ASA ID for an Algorand network.
|
|
265
|
-
|
|
266
|
-
Args:
|
|
267
|
-
network_name: Network name ('algorand' or 'algorand-testnet')
|
|
268
|
-
|
|
269
|
-
Returns:
|
|
270
|
-
USDC ASA ID or None if network not found
|
|
271
|
-
"""
|
|
272
|
-
from uvd_x402_sdk.networks.base import get_network
|
|
273
|
-
|
|
274
|
-
network = get_network(network_name)
|
|
275
|
-
if not network or network.network_type != NetworkType.ALGORAND:
|
|
276
|
-
return None
|
|
277
|
-
|
|
278
|
-
# Try extra_config first, then fall back to usdc_address
|
|
279
|
-
asa_id = network.extra_config.get("usdc_asa_id")
|
|
280
|
-
if asa_id:
|
|
281
|
-
return int(asa_id)
|
|
282
|
-
|
|
283
|
-
# Parse from usdc_address (which stores the ASA ID as string)
|
|
284
|
-
try:
|
|
285
|
-
return int(network.usdc_address)
|
|
286
|
-
except (ValueError, TypeError):
|
|
287
|
-
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/integrations/django_integration.py
RENAMED
|
File without changes
|
{uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/integrations/fastapi_integration.py
RENAMED
|
File without changes
|
{uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/integrations/flask_integration.py
RENAMED
|
File without changes
|
{uvd_x402_sdk-0.4.1 → uvd_x402_sdk-0.4.2}/src/uvd_x402_sdk/integrations/lambda_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|