uvd-x402-sdk 0.4.1__py3-none-any.whl → 0.5.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 +48 -4
- uvd_x402_sdk/facilitator.py +384 -0
- uvd_x402_sdk/networks/algorand.py +363 -53
- uvd_x402_sdk/networks/near.py +46 -0
- uvd_x402_sdk/networks/solana.py +58 -0
- uvd_x402_sdk/networks/stellar.py +46 -0
- {uvd_x402_sdk-0.4.1.dist-info → uvd_x402_sdk-0.5.0.dist-info}/METADATA +4 -2
- {uvd_x402_sdk-0.4.1.dist-info → uvd_x402_sdk-0.5.0.dist-info}/RECORD +11 -10
- {uvd_x402_sdk-0.4.1.dist-info → uvd_x402_sdk-0.5.0.dist-info}/LICENSE +0 -0
- {uvd_x402_sdk-0.4.1.dist-info → uvd_x402_sdk-0.5.0.dist-info}/WHEEL +0 -0
- {uvd_x402_sdk-0.4.1.dist-info → uvd_x402_sdk-0.5.0.dist-info}/top_level.txt +0 -0
uvd_x402_sdk/__init__.py
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
uvd-x402-sdk: Python SDK for x402 payments via Ultravioleta DAO facilitator.
|
|
3
3
|
|
|
4
4
|
This SDK enables developers to easily integrate x402 cryptocurrency payments
|
|
5
|
-
into their Python applications with support for
|
|
6
|
-
|
|
5
|
+
into their Python applications with support for 16 blockchain networks across
|
|
6
|
+
5 network types (EVM, SVM, NEAR, Stellar, Algorand).
|
|
7
|
+
|
|
8
|
+
The SDK automatically handles facilitator configuration - users don't need to
|
|
9
|
+
configure fee payer addresses or other facilitator details manually.
|
|
7
10
|
|
|
8
11
|
Supports both x402 v1 and v2 protocols:
|
|
9
12
|
- v1: network as string ("base", "solana")
|
|
@@ -29,15 +32,16 @@ Example usage:
|
|
|
29
32
|
def protected_endpoint():
|
|
30
33
|
return {"message": "Payment verified!"}
|
|
31
34
|
|
|
32
|
-
Supported Networks (
|
|
35
|
+
Supported Networks (16 total):
|
|
33
36
|
- EVM (10): Base, Ethereum, Polygon, Arbitrum, Optimism, Avalanche, Celo,
|
|
34
37
|
HyperEVM, Unichain, Monad
|
|
35
38
|
- SVM (2): Solana, Fogo
|
|
36
39
|
- NEAR (1): NEAR Protocol
|
|
37
40
|
- Stellar (1): Stellar
|
|
41
|
+
- Algorand (2): Algorand mainnet, Algorand testnet
|
|
38
42
|
"""
|
|
39
43
|
|
|
40
|
-
__version__ = "0.
|
|
44
|
+
__version__ = "0.5.0"
|
|
41
45
|
__author__ = "Ultravioleta DAO"
|
|
42
46
|
|
|
43
47
|
from uvd_x402_sdk.client import X402Client
|
|
@@ -109,6 +113,28 @@ from uvd_x402_sdk.response import (
|
|
|
109
113
|
payment_required_response_v2,
|
|
110
114
|
Payment402BuilderV2,
|
|
111
115
|
)
|
|
116
|
+
from uvd_x402_sdk.facilitator import (
|
|
117
|
+
# Facilitator URL and constants
|
|
118
|
+
DEFAULT_FACILITATOR_URL,
|
|
119
|
+
get_facilitator_url,
|
|
120
|
+
# Fee payer addresses by chain
|
|
121
|
+
ALGORAND_FEE_PAYER_MAINNET,
|
|
122
|
+
ALGORAND_FEE_PAYER_TESTNET,
|
|
123
|
+
SOLANA_FEE_PAYER_MAINNET,
|
|
124
|
+
SOLANA_FEE_PAYER_DEVNET,
|
|
125
|
+
FOGO_FEE_PAYER_MAINNET,
|
|
126
|
+
FOGO_FEE_PAYER_TESTNET,
|
|
127
|
+
NEAR_FEE_PAYER_MAINNET,
|
|
128
|
+
NEAR_FEE_PAYER_TESTNET,
|
|
129
|
+
STELLAR_FEE_PAYER_MAINNET,
|
|
130
|
+
STELLAR_FEE_PAYER_TESTNET,
|
|
131
|
+
# Helper functions
|
|
132
|
+
get_fee_payer,
|
|
133
|
+
get_facilitator_address,
|
|
134
|
+
requires_fee_payer,
|
|
135
|
+
get_all_fee_payers,
|
|
136
|
+
build_payment_info,
|
|
137
|
+
)
|
|
112
138
|
|
|
113
139
|
__all__ = [
|
|
114
140
|
# Version
|
|
@@ -182,4 +208,22 @@ __all__ = [
|
|
|
182
208
|
"create_402_headers_v2",
|
|
183
209
|
"payment_required_response_v2",
|
|
184
210
|
"Payment402BuilderV2",
|
|
211
|
+
# Facilitator constants and helpers
|
|
212
|
+
"DEFAULT_FACILITATOR_URL",
|
|
213
|
+
"get_facilitator_url",
|
|
214
|
+
"ALGORAND_FEE_PAYER_MAINNET",
|
|
215
|
+
"ALGORAND_FEE_PAYER_TESTNET",
|
|
216
|
+
"SOLANA_FEE_PAYER_MAINNET",
|
|
217
|
+
"SOLANA_FEE_PAYER_DEVNET",
|
|
218
|
+
"FOGO_FEE_PAYER_MAINNET",
|
|
219
|
+
"FOGO_FEE_PAYER_TESTNET",
|
|
220
|
+
"NEAR_FEE_PAYER_MAINNET",
|
|
221
|
+
"NEAR_FEE_PAYER_TESTNET",
|
|
222
|
+
"STELLAR_FEE_PAYER_MAINNET",
|
|
223
|
+
"STELLAR_FEE_PAYER_TESTNET",
|
|
224
|
+
"get_fee_payer",
|
|
225
|
+
"get_facilitator_address",
|
|
226
|
+
"requires_fee_payer",
|
|
227
|
+
"get_all_fee_payers",
|
|
228
|
+
"build_payment_info",
|
|
185
229
|
]
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Facilitator configuration and constants.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- Default facilitator URL
|
|
6
|
+
- Fee payer addresses for all supported non-EVM networks
|
|
7
|
+
- Helper functions to get the appropriate facilitator address for any network
|
|
8
|
+
|
|
9
|
+
Users of this SDK should NOT need to configure facilitator details manually.
|
|
10
|
+
All addresses are embedded as constants, extracted from the official facilitator.
|
|
11
|
+
|
|
12
|
+
Facilitator URL: https://facilitator.ultravioletadao.xyz
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Dict, Optional
|
|
16
|
+
|
|
17
|
+
from uvd_x402_sdk.networks.base import (
|
|
18
|
+
NetworkType,
|
|
19
|
+
get_network,
|
|
20
|
+
is_caip2_format,
|
|
21
|
+
normalize_network,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# Facilitator URL
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
DEFAULT_FACILITATOR_URL = "https://facilitator.ultravioletadao.xyz"
|
|
30
|
+
|
|
31
|
+
# Alternative facilitator URLs (for future use)
|
|
32
|
+
FACILITATOR_URLS = {
|
|
33
|
+
"production": "https://facilitator.ultravioletadao.xyz",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# Fee Payer Addresses (Non-EVM chains require fee payer for gasless payments)
|
|
39
|
+
# =============================================================================
|
|
40
|
+
|
|
41
|
+
# Algorand fee payer addresses
|
|
42
|
+
ALGORAND_FEE_PAYER_MAINNET = "KIMS5H6QLCUDL65L5UBTOXDPWLMTS7N3AAC3I6B2NCONEI5QIVK7LH2C2I"
|
|
43
|
+
ALGORAND_FEE_PAYER_TESTNET = "5DPPDQNYUPCTXRZWRYSF3WPYU6RKAUR25F3YG4EKXQRHV5AUAI62H5GXL4"
|
|
44
|
+
|
|
45
|
+
# Solana fee payer addresses (also used for Fogo mainnet)
|
|
46
|
+
SOLANA_FEE_PAYER_MAINNET = "F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq"
|
|
47
|
+
SOLANA_FEE_PAYER_DEVNET = "6xNPewUdKRbEZDReQdpyfNUdgNg8QRc8Mt263T5GZSRv"
|
|
48
|
+
|
|
49
|
+
# Fogo (SVM) fee payer addresses
|
|
50
|
+
FOGO_FEE_PAYER_MAINNET = "F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq"
|
|
51
|
+
FOGO_FEE_PAYER_TESTNET = "6xNPewUdKRbEZDReQdpyfNUdgNg8QRc8Mt263T5GZSRv"
|
|
52
|
+
|
|
53
|
+
# NEAR fee payer addresses (account IDs)
|
|
54
|
+
NEAR_FEE_PAYER_MAINNET = "uvd-facilitator.near"
|
|
55
|
+
NEAR_FEE_PAYER_TESTNET = "uvd-facilitator.testnet"
|
|
56
|
+
|
|
57
|
+
# Stellar fee payer addresses (public keys)
|
|
58
|
+
STELLAR_FEE_PAYER_MAINNET = "GCHPGXJT2WFFRFCA5TV4G4E3PMMXLNIDUH27PKDYA4QJ2XGYZWGFZNHB"
|
|
59
|
+
STELLAR_FEE_PAYER_TESTNET = "GBBFZMLUJEZVI32EN4XA2KPP445XIBTMTRBLYWFIL556RDTHS2OWFQ2Z"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# =============================================================================
|
|
63
|
+
# Network to Fee Payer Mapping
|
|
64
|
+
# =============================================================================
|
|
65
|
+
|
|
66
|
+
# Maps network names to their fee payer addresses
|
|
67
|
+
# EVM networks don't have fee payers (they use EIP-3009 transferWithAuthorization)
|
|
68
|
+
_FEE_PAYER_BY_NETWORK: Dict[str, str] = {
|
|
69
|
+
# Algorand
|
|
70
|
+
"algorand": ALGORAND_FEE_PAYER_MAINNET,
|
|
71
|
+
"algorand-mainnet": ALGORAND_FEE_PAYER_MAINNET,
|
|
72
|
+
"algorand-testnet": ALGORAND_FEE_PAYER_TESTNET,
|
|
73
|
+
# Solana
|
|
74
|
+
"solana": SOLANA_FEE_PAYER_MAINNET,
|
|
75
|
+
"solana-mainnet": SOLANA_FEE_PAYER_MAINNET,
|
|
76
|
+
"solana-devnet": SOLANA_FEE_PAYER_DEVNET,
|
|
77
|
+
# Fogo (SVM)
|
|
78
|
+
"fogo": FOGO_FEE_PAYER_MAINNET,
|
|
79
|
+
"fogo-mainnet": FOGO_FEE_PAYER_MAINNET,
|
|
80
|
+
"fogo-testnet": FOGO_FEE_PAYER_TESTNET,
|
|
81
|
+
# NEAR
|
|
82
|
+
"near": NEAR_FEE_PAYER_MAINNET,
|
|
83
|
+
"near-mainnet": NEAR_FEE_PAYER_MAINNET,
|
|
84
|
+
"near-testnet": NEAR_FEE_PAYER_TESTNET,
|
|
85
|
+
# Stellar
|
|
86
|
+
"stellar": STELLAR_FEE_PAYER_MAINNET,
|
|
87
|
+
"stellar-mainnet": STELLAR_FEE_PAYER_MAINNET,
|
|
88
|
+
"stellar-testnet": STELLAR_FEE_PAYER_TESTNET,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# CAIP-2 format mappings (x402 v2)
|
|
92
|
+
_FEE_PAYER_BY_CAIP2: Dict[str, str] = {
|
|
93
|
+
# Algorand
|
|
94
|
+
"algorand:mainnet": ALGORAND_FEE_PAYER_MAINNET,
|
|
95
|
+
"algorand:testnet": ALGORAND_FEE_PAYER_TESTNET,
|
|
96
|
+
# Solana
|
|
97
|
+
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": SOLANA_FEE_PAYER_MAINNET,
|
|
98
|
+
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": SOLANA_FEE_PAYER_DEVNET,
|
|
99
|
+
# Fogo (SVM)
|
|
100
|
+
"fogo:mainnet": FOGO_FEE_PAYER_MAINNET,
|
|
101
|
+
"fogo:testnet": FOGO_FEE_PAYER_TESTNET,
|
|
102
|
+
# NEAR
|
|
103
|
+
"near:mainnet": NEAR_FEE_PAYER_MAINNET,
|
|
104
|
+
"near:testnet": NEAR_FEE_PAYER_TESTNET,
|
|
105
|
+
# Stellar
|
|
106
|
+
"stellar:pubnet": STELLAR_FEE_PAYER_MAINNET,
|
|
107
|
+
"stellar:testnet": STELLAR_FEE_PAYER_TESTNET,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# =============================================================================
|
|
112
|
+
# Helper Functions
|
|
113
|
+
# =============================================================================
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_fee_payer(network: str) -> Optional[str]:
|
|
117
|
+
"""
|
|
118
|
+
Get the fee payer address for a network.
|
|
119
|
+
|
|
120
|
+
Fee payers are only needed for non-EVM chains (Algorand, Solana, NEAR, Stellar).
|
|
121
|
+
EVM chains use EIP-3009 transferWithAuthorization which is gasless by design.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
network: Network identifier (v1 name or CAIP-2 format)
|
|
125
|
+
Examples: "algorand", "algorand-mainnet", "algorand:mainnet"
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Fee payer address if applicable, None for EVM chains
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
>>> get_fee_payer("algorand")
|
|
132
|
+
'KIMS5H6QLCUDL65L5UBTOXDPWLMTS7N3AAC3I6B2NCONEI5QIVK7LH2C2I'
|
|
133
|
+
>>> get_fee_payer("solana")
|
|
134
|
+
'F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq'
|
|
135
|
+
>>> get_fee_payer("base") # EVM chain
|
|
136
|
+
None
|
|
137
|
+
"""
|
|
138
|
+
# Check CAIP-2 format first
|
|
139
|
+
if is_caip2_format(network):
|
|
140
|
+
return _FEE_PAYER_BY_CAIP2.get(network)
|
|
141
|
+
|
|
142
|
+
# Check v1 network name
|
|
143
|
+
network_lower = network.lower()
|
|
144
|
+
return _FEE_PAYER_BY_NETWORK.get(network_lower)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_facilitator_address(network: str) -> Optional[str]:
|
|
148
|
+
"""
|
|
149
|
+
Alias for get_fee_payer() for backward compatibility.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
network: Network identifier
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Facilitator/fee payer address if applicable
|
|
156
|
+
"""
|
|
157
|
+
return get_fee_payer(network)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def requires_fee_payer(network: str) -> bool:
|
|
161
|
+
"""
|
|
162
|
+
Check if a network requires a fee payer address.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
network: Network identifier (v1 name or CAIP-2 format)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if network requires fee payer (non-EVM), False otherwise
|
|
169
|
+
|
|
170
|
+
Example:
|
|
171
|
+
>>> requires_fee_payer("algorand")
|
|
172
|
+
True
|
|
173
|
+
>>> requires_fee_payer("base")
|
|
174
|
+
False
|
|
175
|
+
"""
|
|
176
|
+
# Try to get network config
|
|
177
|
+
try:
|
|
178
|
+
normalized = normalize_network(network)
|
|
179
|
+
net_config = get_network(normalized)
|
|
180
|
+
if net_config:
|
|
181
|
+
return net_config.network_type != NetworkType.EVM
|
|
182
|
+
except ValueError:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
# Fall back to checking if we have a fee payer registered
|
|
186
|
+
return get_fee_payer(network) is not None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_network_type_from_fee_payer(address: str) -> Optional[NetworkType]:
|
|
190
|
+
"""
|
|
191
|
+
Determine network type from a fee payer address format.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
address: Fee payer address
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
NetworkType if recognizable, None otherwise
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
>>> get_network_type_from_fee_payer("KIMS5H6QLCUDL65L5UBTOXDPWLMTS7N3AAC3I6B2NCONEI5QIVK7LH2C2I")
|
|
201
|
+
NetworkType.ALGORAND
|
|
202
|
+
>>> get_network_type_from_fee_payer("F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq")
|
|
203
|
+
NetworkType.SVM
|
|
204
|
+
"""
|
|
205
|
+
if not address:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
# Algorand: 58 characters, base32 (A-Z2-7)
|
|
209
|
+
if len(address) == 58 and address.isalnum():
|
|
210
|
+
import re
|
|
211
|
+
if re.match(r'^[A-Z2-7]+$', address):
|
|
212
|
+
return NetworkType.ALGORAND
|
|
213
|
+
|
|
214
|
+
# Solana/Fogo: 32-44 characters, base58
|
|
215
|
+
if 32 <= len(address) <= 44:
|
|
216
|
+
# Base58 uses alphanumeric chars except 0, O, I, l
|
|
217
|
+
import re
|
|
218
|
+
if re.match(r'^[A-HJ-NP-Za-km-z1-9]+$', address):
|
|
219
|
+
return NetworkType.SVM
|
|
220
|
+
|
|
221
|
+
# NEAR: ends with .near or .testnet
|
|
222
|
+
if address.endswith('.near') or address.endswith('.testnet'):
|
|
223
|
+
return NetworkType.NEAR
|
|
224
|
+
|
|
225
|
+
# Stellar: starts with G, 56 characters
|
|
226
|
+
if len(address) == 56 and address.startswith('G'):
|
|
227
|
+
return NetworkType.STELLAR
|
|
228
|
+
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def validate_fee_payer_for_network(network: str, address: str) -> bool:
|
|
233
|
+
"""
|
|
234
|
+
Validate that a fee payer address format matches the network type.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
network: Network identifier
|
|
238
|
+
address: Fee payer address to validate
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
True if address format is valid for network
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
>>> validate_fee_payer_for_network("algorand", "KIMS5H6...")
|
|
245
|
+
True
|
|
246
|
+
>>> validate_fee_payer_for_network("algorand", "F742C4V...") # Solana address
|
|
247
|
+
False
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
normalized = normalize_network(network)
|
|
251
|
+
net_config = get_network(normalized)
|
|
252
|
+
if not net_config:
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
detected_type = get_network_type_from_fee_payer(address)
|
|
256
|
+
if detected_type is None:
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
# SVM includes both SOLANA and SVM types
|
|
260
|
+
if net_config.network_type in (NetworkType.SVM, NetworkType.SOLANA):
|
|
261
|
+
return detected_type == NetworkType.SVM
|
|
262
|
+
|
|
263
|
+
return net_config.network_type == detected_type
|
|
264
|
+
except ValueError:
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_all_fee_payers() -> Dict[str, str]:
|
|
269
|
+
"""
|
|
270
|
+
Get all registered fee payer addresses.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Dictionary mapping network names to fee payer addresses
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
>>> payers = get_all_fee_payers()
|
|
277
|
+
>>> for network, address in payers.items():
|
|
278
|
+
... print(f"{network}: {address}")
|
|
279
|
+
"""
|
|
280
|
+
return dict(_FEE_PAYER_BY_NETWORK)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def get_facilitator_url() -> str:
|
|
284
|
+
"""
|
|
285
|
+
Get the default facilitator URL.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Facilitator API URL
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
>>> url = get_facilitator_url()
|
|
292
|
+
>>> print(url)
|
|
293
|
+
'https://facilitator.ultravioletadao.xyz'
|
|
294
|
+
"""
|
|
295
|
+
return DEFAULT_FACILITATOR_URL
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# =============================================================================
|
|
299
|
+
# Payment Info Builder
|
|
300
|
+
# =============================================================================
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def build_payment_info(
|
|
304
|
+
network: str,
|
|
305
|
+
pay_to: str,
|
|
306
|
+
max_amount_required: str,
|
|
307
|
+
description: str = "",
|
|
308
|
+
resource: str = "",
|
|
309
|
+
asset: Optional[str] = None,
|
|
310
|
+
token_type: str = "usdc",
|
|
311
|
+
extra: Optional[Dict] = None,
|
|
312
|
+
) -> Dict:
|
|
313
|
+
"""
|
|
314
|
+
Build a payment info dict with all facilitator details pre-configured.
|
|
315
|
+
|
|
316
|
+
This function automatically includes the correct fee payer address for
|
|
317
|
+
non-EVM networks, so SDK users don't need to configure this manually.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
network: Network identifier (e.g., "algorand", "solana", "base")
|
|
321
|
+
pay_to: Recipient address
|
|
322
|
+
max_amount_required: Maximum amount in token units (string)
|
|
323
|
+
description: Optional payment description
|
|
324
|
+
resource: Optional resource being purchased
|
|
325
|
+
asset: Optional token address (defaults to USDC for network)
|
|
326
|
+
token_type: Token type (default "usdc")
|
|
327
|
+
extra: Additional extra fields to include
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Payment info dictionary ready for 402 response
|
|
331
|
+
|
|
332
|
+
Example:
|
|
333
|
+
>>> info = build_payment_info(
|
|
334
|
+
... network="algorand",
|
|
335
|
+
... pay_to="MERCHANT_ADDRESS...",
|
|
336
|
+
... max_amount_required="1000000",
|
|
337
|
+
... description="API access"
|
|
338
|
+
... )
|
|
339
|
+
>>> # info includes facilitator fee payer automatically
|
|
340
|
+
"""
|
|
341
|
+
# Get network config for defaults
|
|
342
|
+
try:
|
|
343
|
+
normalized = normalize_network(network)
|
|
344
|
+
net_config = get_network(normalized)
|
|
345
|
+
except ValueError:
|
|
346
|
+
net_config = None
|
|
347
|
+
|
|
348
|
+
# Build base payment info
|
|
349
|
+
payment_info: Dict = {
|
|
350
|
+
"network": network,
|
|
351
|
+
"payTo": pay_to,
|
|
352
|
+
"maxAmountRequired": max_amount_required,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if description:
|
|
356
|
+
payment_info["description"] = description
|
|
357
|
+
if resource:
|
|
358
|
+
payment_info["resource"] = resource
|
|
359
|
+
|
|
360
|
+
# Set asset (token address)
|
|
361
|
+
if asset:
|
|
362
|
+
payment_info["asset"] = asset
|
|
363
|
+
elif net_config:
|
|
364
|
+
payment_info["asset"] = net_config.usdc_address
|
|
365
|
+
|
|
366
|
+
# Build extra field
|
|
367
|
+
payment_extra: Dict = {}
|
|
368
|
+
|
|
369
|
+
# Add token type info
|
|
370
|
+
payment_extra["token"] = token_type
|
|
371
|
+
|
|
372
|
+
# Add fee payer for non-EVM networks
|
|
373
|
+
fee_payer = get_fee_payer(network)
|
|
374
|
+
if fee_payer:
|
|
375
|
+
payment_extra["feePayer"] = fee_payer
|
|
376
|
+
|
|
377
|
+
# Merge user-provided extra
|
|
378
|
+
if extra:
|
|
379
|
+
payment_extra.update(extra)
|
|
380
|
+
|
|
381
|
+
if payment_extra:
|
|
382
|
+
payment_info["extra"] = payment_extra
|
|
383
|
+
|
|
384
|
+
return payment_info
|
|
@@ -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.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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,
|
|
@@ -35,6 +53,20 @@ from uvd_x402_sdk.networks.base import (
|
|
|
35
53
|
register_network,
|
|
36
54
|
)
|
|
37
55
|
|
|
56
|
+
# Algorand fee payer addresses are defined in uvd_x402_sdk.facilitator
|
|
57
|
+
# Import here for convenience
|
|
58
|
+
try:
|
|
59
|
+
from uvd_x402_sdk.facilitator import (
|
|
60
|
+
ALGORAND_FEE_PAYER_MAINNET,
|
|
61
|
+
ALGORAND_FEE_PAYER_TESTNET,
|
|
62
|
+
get_fee_payer,
|
|
63
|
+
)
|
|
64
|
+
except ImportError:
|
|
65
|
+
# Fallback if facilitator module not loaded yet
|
|
66
|
+
ALGORAND_FEE_PAYER_MAINNET = "KIMS5H6QLCUDL65L5UBTOXDPWLMTS7N3AAC3I6B2NCONEI5QIVK7LH2C2I"
|
|
67
|
+
ALGORAND_FEE_PAYER_TESTNET = "5DPPDQNYUPCTXRZWRYSF3WPYU6RKAUR25F3YG4EKXQRHV5AUAI62H5GXL4"
|
|
68
|
+
get_fee_payer = None # type: ignore
|
|
69
|
+
|
|
38
70
|
|
|
39
71
|
# =============================================================================
|
|
40
72
|
# Algorand Networks Configuration
|
|
@@ -63,6 +95,8 @@ ALGORAND = NetworkConfig(
|
|
|
63
95
|
"genesis_id": "mainnet-v1.0",
|
|
64
96
|
# Genesis hash (for CAIP-2)
|
|
65
97
|
"genesis_hash": "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
|
|
98
|
+
# x402 network name (facilitator expects this format)
|
|
99
|
+
"x402_network": "algorand-mainnet",
|
|
66
100
|
},
|
|
67
101
|
)
|
|
68
102
|
|
|
@@ -89,6 +123,8 @@ ALGORAND_TESTNET = NetworkConfig(
|
|
|
89
123
|
"genesis_id": "testnet-v1.0",
|
|
90
124
|
# Genesis hash
|
|
91
125
|
"genesis_hash": "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
|
|
126
|
+
# x402 network name (facilitator expects this format)
|
|
127
|
+
"x402_network": "algorand-testnet",
|
|
92
128
|
},
|
|
93
129
|
)
|
|
94
130
|
|
|
@@ -97,6 +133,34 @@ register_network(ALGORAND)
|
|
|
97
133
|
register_network(ALGORAND_TESTNET)
|
|
98
134
|
|
|
99
135
|
|
|
136
|
+
# =============================================================================
|
|
137
|
+
# Algorand Payment Payload (x402-avm Atomic Group Spec)
|
|
138
|
+
# =============================================================================
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class AlgorandPaymentPayload:
|
|
143
|
+
"""
|
|
144
|
+
Algorand payment payload for x402 atomic group format.
|
|
145
|
+
|
|
146
|
+
Attributes:
|
|
147
|
+
payment_index: Index of the payment transaction in the group (typically 1)
|
|
148
|
+
payment_group: List of base64-encoded msgpack transactions
|
|
149
|
+
[0] = unsigned fee transaction
|
|
150
|
+
[1] = signed ASA transfer transaction
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
payment_index: int
|
|
154
|
+
payment_group: List[str]
|
|
155
|
+
|
|
156
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
157
|
+
"""Convert to dictionary for JSON serialization."""
|
|
158
|
+
return {
|
|
159
|
+
"paymentIndex": self.payment_index,
|
|
160
|
+
"paymentGroup": self.payment_group,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
100
164
|
# =============================================================================
|
|
101
165
|
# Algorand-specific utilities
|
|
102
166
|
# =============================================================================
|
|
@@ -162,59 +226,99 @@ def is_valid_algorand_address(address: str) -> bool:
|
|
|
162
226
|
|
|
163
227
|
def validate_algorand_payload(payload: Dict[str, Any]) -> bool:
|
|
164
228
|
"""
|
|
165
|
-
Validate an Algorand payment payload structure.
|
|
229
|
+
Validate an Algorand payment payload structure (x402-avm atomic group format).
|
|
166
230
|
|
|
167
231
|
The payload must contain:
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
- signedTxn: Base64-encoded signed transaction
|
|
232
|
+
- paymentIndex: Index of the payment transaction (typically 1)
|
|
233
|
+
- paymentGroup: List of base64-encoded msgpack transactions
|
|
234
|
+
- [0]: Unsigned fee transaction (facilitator -> facilitator)
|
|
235
|
+
- [1]: Signed ASA transfer (client -> merchant)
|
|
173
236
|
|
|
174
237
|
Args:
|
|
175
|
-
payload: Payload dictionary from x402 payment
|
|
238
|
+
payload: Payload dictionary from x402 payment (the inner "payload" field)
|
|
176
239
|
|
|
177
240
|
Returns:
|
|
178
241
|
True if valid, raises ValueError if invalid
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
>>> payload = {
|
|
245
|
+
... "paymentIndex": 1,
|
|
246
|
+
... "paymentGroup": [
|
|
247
|
+
... "base64-unsigned-fee-tx...",
|
|
248
|
+
... "base64-signed-payment-tx..."
|
|
249
|
+
... ]
|
|
250
|
+
... }
|
|
251
|
+
>>> validate_algorand_payload(payload)
|
|
252
|
+
True
|
|
179
253
|
"""
|
|
180
|
-
|
|
254
|
+
# Check required fields
|
|
255
|
+
if "paymentIndex" not in payload:
|
|
256
|
+
raise ValueError("Algorand payload missing 'paymentIndex' field")
|
|
257
|
+
if "paymentGroup" not in payload:
|
|
258
|
+
raise ValueError("Algorand payload missing 'paymentGroup' field")
|
|
259
|
+
|
|
260
|
+
# Validate paymentIndex
|
|
261
|
+
payment_index = payload["paymentIndex"]
|
|
262
|
+
if not isinstance(payment_index, int) or payment_index < 0:
|
|
263
|
+
raise ValueError(f"paymentIndex must be a non-negative integer: {payment_index}")
|
|
264
|
+
|
|
265
|
+
# Validate paymentGroup
|
|
266
|
+
payment_group = payload["paymentGroup"]
|
|
267
|
+
if not isinstance(payment_group, list):
|
|
268
|
+
raise ValueError("paymentGroup must be a list")
|
|
269
|
+
|
|
270
|
+
if len(payment_group) < 2:
|
|
271
|
+
raise ValueError(
|
|
272
|
+
f"paymentGroup must contain at least 2 transactions, got {len(payment_group)}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if payment_index >= len(payment_group):
|
|
276
|
+
raise ValueError(
|
|
277
|
+
f"paymentIndex ({payment_index}) out of range for paymentGroup "
|
|
278
|
+
f"(length {len(payment_group)})"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Validate each transaction in the group is valid base64
|
|
282
|
+
for i, txn_b64 in enumerate(payment_group):
|
|
283
|
+
if not isinstance(txn_b64, str):
|
|
284
|
+
raise ValueError(f"paymentGroup[{i}] must be a string")
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
txn_bytes = base64.b64decode(txn_b64)
|
|
288
|
+
if len(txn_bytes) < 50:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
f"paymentGroup[{i}] transaction too short: {len(txn_bytes)} bytes"
|
|
291
|
+
)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"paymentGroup[{i}] is not valid base64: {e}"
|
|
295
|
+
) from e
|
|
181
296
|
|
|
182
|
-
|
|
183
|
-
if field not in payload:
|
|
184
|
-
raise ValueError(f"Algorand payload missing '{field}' field")
|
|
297
|
+
return True
|
|
185
298
|
|
|
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
299
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
300
|
+
def get_x402_network_name(network_name: str) -> str:
|
|
301
|
+
"""
|
|
302
|
+
Get the x402 network name for an Algorand network.
|
|
199
303
|
|
|
200
|
-
|
|
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
|
|
304
|
+
The facilitator expects "algorand-mainnet" or "algorand-testnet".
|
|
207
305
|
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
306
|
+
Args:
|
|
307
|
+
network_name: SDK network name ('algorand' or 'algorand-testnet')
|
|
216
308
|
|
|
217
|
-
|
|
309
|
+
Returns:
|
|
310
|
+
x402 network name ('algorand-mainnet' or 'algorand-testnet')
|
|
311
|
+
"""
|
|
312
|
+
from uvd_x402_sdk.networks.base import get_network
|
|
313
|
+
|
|
314
|
+
network = get_network(network_name)
|
|
315
|
+
if not network:
|
|
316
|
+
# Default mapping
|
|
317
|
+
if network_name == "algorand":
|
|
318
|
+
return "algorand-mainnet"
|
|
319
|
+
return network_name
|
|
320
|
+
|
|
321
|
+
return network.extra_config.get("x402_network", network_name)
|
|
218
322
|
|
|
219
323
|
|
|
220
324
|
def get_explorer_tx_url(network_name: str, tx_id: str) -> Optional[str]:
|
|
@@ -285,3 +389,209 @@ def get_usdc_asa_id(network_name: str) -> Optional[int]:
|
|
|
285
389
|
return int(network.usdc_address)
|
|
286
390
|
except (ValueError, TypeError):
|
|
287
391
|
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# =============================================================================
|
|
395
|
+
# Atomic Group Builder (requires algosdk)
|
|
396
|
+
# =============================================================================
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def build_atomic_group(
|
|
400
|
+
sender_address: str,
|
|
401
|
+
recipient_address: str,
|
|
402
|
+
amount: int,
|
|
403
|
+
asset_id: int,
|
|
404
|
+
facilitator_address: str,
|
|
405
|
+
sign_transaction: Callable,
|
|
406
|
+
algod_client: Optional[Any] = None,
|
|
407
|
+
suggested_params: Optional[Any] = None,
|
|
408
|
+
) -> AlgorandPaymentPayload:
|
|
409
|
+
"""
|
|
410
|
+
Build an Algorand atomic group for x402 payment.
|
|
411
|
+
|
|
412
|
+
This creates the two-transaction atomic group required by the facilitator:
|
|
413
|
+
- Transaction 0: Unsigned fee payment (facilitator -> facilitator, 0 amount)
|
|
414
|
+
- Transaction 1: Signed ASA transfer (sender -> recipient)
|
|
415
|
+
|
|
416
|
+
Requires: pip install py-algorand-sdk
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
sender_address: Client's Algorand address
|
|
420
|
+
recipient_address: Merchant's Algorand address (from payTo)
|
|
421
|
+
amount: Amount in micro-units (1 USDC = 1,000,000)
|
|
422
|
+
asset_id: USDC ASA ID (31566704 mainnet, 10458941 testnet)
|
|
423
|
+
facilitator_address: Facilitator address (from extra.feePayer)
|
|
424
|
+
sign_transaction: Function that signs a transaction.
|
|
425
|
+
Signature: (transaction) -> SignedTransaction
|
|
426
|
+
Can use algosdk's transaction.sign(private_key)
|
|
427
|
+
algod_client: Optional AlgodClient for getting suggested params.
|
|
428
|
+
If not provided, suggested_params must be given.
|
|
429
|
+
suggested_params: Optional SuggestedParams. If not provided,
|
|
430
|
+
algod_client.suggested_params() is called.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
AlgorandPaymentPayload with paymentIndex and paymentGroup
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
ImportError: If algosdk is not installed
|
|
437
|
+
ValueError: If neither algod_client nor suggested_params provided
|
|
438
|
+
|
|
439
|
+
Example:
|
|
440
|
+
>>> from algosdk import transaction
|
|
441
|
+
>>> from algosdk.v2client import algod
|
|
442
|
+
>>>
|
|
443
|
+
>>> client = algod.AlgodClient("", "https://mainnet-api.algonode.cloud")
|
|
444
|
+
>>> payload = build_atomic_group(
|
|
445
|
+
... sender_address="SENDER...",
|
|
446
|
+
... recipient_address="MERCHANT...",
|
|
447
|
+
... amount=1000000, # 1 USDC
|
|
448
|
+
... asset_id=31566704,
|
|
449
|
+
... facilitator_address="FACILITATOR...",
|
|
450
|
+
... sign_transaction=lambda txn: txn.sign(private_key),
|
|
451
|
+
... algod_client=client,
|
|
452
|
+
... )
|
|
453
|
+
"""
|
|
454
|
+
try:
|
|
455
|
+
from algosdk import encoding, transaction
|
|
456
|
+
except ImportError as e:
|
|
457
|
+
raise ImportError(
|
|
458
|
+
"algosdk is required for building atomic groups. "
|
|
459
|
+
"Install with: pip install py-algorand-sdk"
|
|
460
|
+
) from e
|
|
461
|
+
|
|
462
|
+
# Get suggested params
|
|
463
|
+
if suggested_params is None:
|
|
464
|
+
if algod_client is None:
|
|
465
|
+
raise ValueError(
|
|
466
|
+
"Either algod_client or suggested_params must be provided"
|
|
467
|
+
)
|
|
468
|
+
suggested_params = algod_client.suggested_params()
|
|
469
|
+
|
|
470
|
+
# Transaction 0: Fee payment (facilitator -> facilitator, 0 amount)
|
|
471
|
+
# This transaction pays fees for both txns in the group
|
|
472
|
+
fee_txn = transaction.PaymentTxn(
|
|
473
|
+
sender=facilitator_address,
|
|
474
|
+
receiver=facilitator_address, # self-transfer
|
|
475
|
+
amt=0,
|
|
476
|
+
sp=suggested_params,
|
|
477
|
+
)
|
|
478
|
+
# Cover both transactions (1000 microAlgos each = 2000 total)
|
|
479
|
+
fee_txn.fee = 2000
|
|
480
|
+
|
|
481
|
+
# Transaction 1: ASA transfer (client -> merchant)
|
|
482
|
+
payment_txn = transaction.AssetTransferTxn(
|
|
483
|
+
sender=sender_address,
|
|
484
|
+
receiver=recipient_address,
|
|
485
|
+
amt=amount,
|
|
486
|
+
index=asset_id,
|
|
487
|
+
sp=suggested_params,
|
|
488
|
+
)
|
|
489
|
+
# Fee paid by transaction 0
|
|
490
|
+
payment_txn.fee = 0
|
|
491
|
+
|
|
492
|
+
# Assign group ID to both transactions
|
|
493
|
+
group_id = transaction.calculate_group_id([fee_txn, payment_txn])
|
|
494
|
+
fee_txn.group = group_id
|
|
495
|
+
payment_txn.group = group_id
|
|
496
|
+
|
|
497
|
+
# Encode fee transaction (UNSIGNED - facilitator will sign)
|
|
498
|
+
unsigned_fee_txn_bytes = encoding.msgpack_encode(fee_txn)
|
|
499
|
+
unsigned_fee_txn_base64 = base64.b64encode(unsigned_fee_txn_bytes).decode("utf-8")
|
|
500
|
+
|
|
501
|
+
# Sign and encode payment transaction
|
|
502
|
+
signed_payment_txn = sign_transaction(payment_txn)
|
|
503
|
+
signed_payment_txn_bytes = encoding.msgpack_encode(signed_payment_txn)
|
|
504
|
+
signed_payment_txn_base64 = base64.b64encode(signed_payment_txn_bytes).decode("utf-8")
|
|
505
|
+
|
|
506
|
+
return AlgorandPaymentPayload(
|
|
507
|
+
payment_index=1, # Index of the payment transaction
|
|
508
|
+
payment_group=[
|
|
509
|
+
unsigned_fee_txn_base64, # Transaction 0: unsigned fee tx
|
|
510
|
+
signed_payment_txn_base64, # Transaction 1: signed payment tx
|
|
511
|
+
],
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def create_private_key_signer(private_key: str) -> Callable:
|
|
516
|
+
"""
|
|
517
|
+
Create a transaction signer from a private key.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
private_key: Algorand private key (base64 encoded)
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Function that signs transactions
|
|
524
|
+
|
|
525
|
+
Example:
|
|
526
|
+
>>> signer = create_private_key_signer(my_private_key)
|
|
527
|
+
>>> payload = build_atomic_group(..., sign_transaction=signer)
|
|
528
|
+
"""
|
|
529
|
+
def sign(txn: Any) -> Any:
|
|
530
|
+
return txn.sign(private_key)
|
|
531
|
+
return sign
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def build_x402_payment_request(
|
|
535
|
+
payload: AlgorandPaymentPayload,
|
|
536
|
+
network: str = "algorand-mainnet",
|
|
537
|
+
scheme: str = "exact",
|
|
538
|
+
version: int = 1,
|
|
539
|
+
) -> Dict[str, Any]:
|
|
540
|
+
"""
|
|
541
|
+
Build a complete x402 payment request for Algorand.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
payload: AlgorandPaymentPayload from build_atomic_group()
|
|
545
|
+
network: Network name ("algorand-mainnet" or "algorand-testnet")
|
|
546
|
+
scheme: Payment scheme (default "exact")
|
|
547
|
+
version: x402 version (default 1)
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Complete x402 payment request dictionary
|
|
551
|
+
|
|
552
|
+
Example:
|
|
553
|
+
>>> payload = build_atomic_group(...)
|
|
554
|
+
>>> request = build_x402_payment_request(payload)
|
|
555
|
+
>>> # Send as X-PAYMENT header (base64 encoded JSON)
|
|
556
|
+
>>> import json, base64
|
|
557
|
+
>>> header = base64.b64encode(json.dumps(request).encode()).decode()
|
|
558
|
+
"""
|
|
559
|
+
return {
|
|
560
|
+
"x402Version": version,
|
|
561
|
+
"scheme": scheme,
|
|
562
|
+
"network": network,
|
|
563
|
+
"payload": payload.to_dict(),
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def get_algorand_fee_payer(network_name: str = "algorand") -> str:
|
|
568
|
+
"""
|
|
569
|
+
Get the fee payer address for an Algorand network.
|
|
570
|
+
|
|
571
|
+
The fee payer is the facilitator address that pays transaction fees
|
|
572
|
+
for the atomic group. This address is used to construct Transaction 0
|
|
573
|
+
(the fee payment transaction).
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
network_name: Network name ('algorand' or 'algorand-testnet')
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
Fee payer address for the specified network
|
|
580
|
+
|
|
581
|
+
Example:
|
|
582
|
+
>>> get_algorand_fee_payer("algorand")
|
|
583
|
+
'KIMS5H6QLCUDL65L5UBTOXDPWLMTS7N3AAC3I6B2NCONEI5QIVK7LH2C2I'
|
|
584
|
+
>>> get_algorand_fee_payer("algorand-testnet")
|
|
585
|
+
'5DPPDQNYUPCTXRZWRYSF3WPYU6RKAUR25F3YG4EKXQRHV5AUAI62H5GXL4'
|
|
586
|
+
"""
|
|
587
|
+
# Use facilitator module if available
|
|
588
|
+
if get_fee_payer is not None:
|
|
589
|
+
fee_payer = get_fee_payer(network_name)
|
|
590
|
+
if fee_payer:
|
|
591
|
+
return fee_payer
|
|
592
|
+
|
|
593
|
+
# Fallback to direct lookup
|
|
594
|
+
network_lower = network_name.lower()
|
|
595
|
+
if "testnet" in network_lower:
|
|
596
|
+
return ALGORAND_FEE_PAYER_TESTNET
|
|
597
|
+
return ALGORAND_FEE_PAYER_MAINNET
|
uvd_x402_sdk/networks/near.py
CHANGED
|
@@ -32,6 +32,20 @@ from uvd_x402_sdk.networks.base import (
|
|
|
32
32
|
register_network,
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
+
# NEAR fee payer addresses are defined in uvd_x402_sdk.facilitator
|
|
36
|
+
# Import here for convenience
|
|
37
|
+
try:
|
|
38
|
+
from uvd_x402_sdk.facilitator import (
|
|
39
|
+
NEAR_FEE_PAYER_MAINNET,
|
|
40
|
+
NEAR_FEE_PAYER_TESTNET,
|
|
41
|
+
get_fee_payer,
|
|
42
|
+
)
|
|
43
|
+
except ImportError:
|
|
44
|
+
# Fallback if facilitator module not loaded yet
|
|
45
|
+
NEAR_FEE_PAYER_MAINNET = "uvd-facilitator.near"
|
|
46
|
+
NEAR_FEE_PAYER_TESTNET = "uvd-facilitator.testnet"
|
|
47
|
+
get_fee_payer = None # type: ignore
|
|
48
|
+
|
|
35
49
|
# NEP-366 hash prefix: (2^30 + 366) = 1073742190
|
|
36
50
|
NEP366_PREFIX = ((2**30) + 366).to_bytes(4, 'little')
|
|
37
51
|
|
|
@@ -395,3 +409,35 @@ def is_valid_near_account_id(account_id: str) -> bool:
|
|
|
395
409
|
|
|
396
410
|
allowed = set('abcdefghijklmnopqrstuvwxyz0123456789_-.')
|
|
397
411
|
return all(c in allowed for c in account_id)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def get_near_fee_payer(network_name: str = "near") -> str:
|
|
415
|
+
"""
|
|
416
|
+
Get the fee payer account ID for a NEAR network.
|
|
417
|
+
|
|
418
|
+
The fee payer is the facilitator account that pays gas fees.
|
|
419
|
+
This account wraps the SignedDelegateAction and submits it.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
network_name: Network name ('near' or 'near-testnet')
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Fee payer account ID for the specified network
|
|
426
|
+
|
|
427
|
+
Example:
|
|
428
|
+
>>> get_near_fee_payer("near")
|
|
429
|
+
'uvd-facilitator.near'
|
|
430
|
+
>>> get_near_fee_payer("near-testnet")
|
|
431
|
+
'uvd-facilitator.testnet'
|
|
432
|
+
"""
|
|
433
|
+
# Use facilitator module if available
|
|
434
|
+
if get_fee_payer is not None:
|
|
435
|
+
fee_payer = get_fee_payer(network_name)
|
|
436
|
+
if fee_payer:
|
|
437
|
+
return fee_payer
|
|
438
|
+
|
|
439
|
+
# Fallback to direct lookup
|
|
440
|
+
network_lower = network_name.lower()
|
|
441
|
+
if "testnet" in network_lower:
|
|
442
|
+
return NEAR_FEE_PAYER_TESTNET
|
|
443
|
+
return NEAR_FEE_PAYER_MAINNET
|
uvd_x402_sdk/networks/solana.py
CHANGED
|
@@ -38,6 +38,24 @@ from uvd_x402_sdk.networks.base import (
|
|
|
38
38
|
register_network,
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
+
# SVM fee payer addresses are defined in uvd_x402_sdk.facilitator
|
|
42
|
+
# Import here for convenience
|
|
43
|
+
try:
|
|
44
|
+
from uvd_x402_sdk.facilitator import (
|
|
45
|
+
SOLANA_FEE_PAYER_MAINNET,
|
|
46
|
+
SOLANA_FEE_PAYER_DEVNET,
|
|
47
|
+
FOGO_FEE_PAYER_MAINNET,
|
|
48
|
+
FOGO_FEE_PAYER_TESTNET,
|
|
49
|
+
get_fee_payer,
|
|
50
|
+
)
|
|
51
|
+
except ImportError:
|
|
52
|
+
# Fallback if facilitator module not loaded yet
|
|
53
|
+
SOLANA_FEE_PAYER_MAINNET = "F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq"
|
|
54
|
+
SOLANA_FEE_PAYER_DEVNET = "6xNPewUdKRbEZDReQdpyfNUdgNg8QRc8Mt263T5GZSRv"
|
|
55
|
+
FOGO_FEE_PAYER_MAINNET = "F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq"
|
|
56
|
+
FOGO_FEE_PAYER_TESTNET = "6xNPewUdKRbEZDReQdpyfNUdgNg8QRc8Mt263T5GZSRv"
|
|
57
|
+
get_fee_payer = None # type: ignore
|
|
58
|
+
|
|
41
59
|
|
|
42
60
|
# =============================================================================
|
|
43
61
|
# SVM Networks Configuration
|
|
@@ -351,3 +369,43 @@ def is_token_2022(token_type: str) -> bool:
|
|
|
351
369
|
True if token uses Token2022, False for standard SPL
|
|
352
370
|
"""
|
|
353
371
|
return token_type.lower() in TOKEN_2022_TOKENS
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def get_svm_fee_payer(network_name: str = "solana") -> str:
|
|
375
|
+
"""
|
|
376
|
+
Get the fee payer address for an SVM network.
|
|
377
|
+
|
|
378
|
+
The fee payer is the facilitator address that pays transaction fees.
|
|
379
|
+
This address should be used as the fee payer in VersionedTransaction.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
network_name: Network name ('solana', 'solana-devnet', 'fogo', 'fogo-testnet')
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Fee payer address for the specified network
|
|
386
|
+
|
|
387
|
+
Example:
|
|
388
|
+
>>> get_svm_fee_payer("solana")
|
|
389
|
+
'F742C4VfFLQ9zRQyithoj5229ZgtX2WqKCSFKgH2EThq'
|
|
390
|
+
>>> get_svm_fee_payer("solana-devnet")
|
|
391
|
+
'6xNPewUdKRbEZDReQdpyfNUdgNg8QRc8Mt263T5GZSRv'
|
|
392
|
+
"""
|
|
393
|
+
# Use facilitator module if available
|
|
394
|
+
if get_fee_payer is not None:
|
|
395
|
+
fee_payer = get_fee_payer(network_name)
|
|
396
|
+
if fee_payer:
|
|
397
|
+
return fee_payer
|
|
398
|
+
|
|
399
|
+
# Fallback to direct lookup
|
|
400
|
+
network_lower = network_name.lower()
|
|
401
|
+
if "fogo" in network_lower:
|
|
402
|
+
if "testnet" in network_lower:
|
|
403
|
+
return FOGO_FEE_PAYER_TESTNET
|
|
404
|
+
return FOGO_FEE_PAYER_MAINNET
|
|
405
|
+
elif "devnet" in network_lower:
|
|
406
|
+
return SOLANA_FEE_PAYER_DEVNET
|
|
407
|
+
return SOLANA_FEE_PAYER_MAINNET
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# Alias for backward compatibility
|
|
411
|
+
get_solana_fee_payer = get_svm_fee_payer
|
uvd_x402_sdk/networks/stellar.py
CHANGED
|
@@ -19,6 +19,20 @@ from uvd_x402_sdk.networks.base import (
|
|
|
19
19
|
register_network,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
+
# Stellar fee payer addresses are defined in uvd_x402_sdk.facilitator
|
|
23
|
+
# Import here for convenience
|
|
24
|
+
try:
|
|
25
|
+
from uvd_x402_sdk.facilitator import (
|
|
26
|
+
STELLAR_FEE_PAYER_MAINNET,
|
|
27
|
+
STELLAR_FEE_PAYER_TESTNET,
|
|
28
|
+
get_fee_payer,
|
|
29
|
+
)
|
|
30
|
+
except ImportError:
|
|
31
|
+
# Fallback if facilitator module not loaded yet
|
|
32
|
+
STELLAR_FEE_PAYER_MAINNET = "GCHPGXJT2WFFRFCA5TV4G4E3PMMXLNIDUH27PKDYA4QJ2XGYZWGFZNHB"
|
|
33
|
+
STELLAR_FEE_PAYER_TESTNET = "GBBFZMLUJEZVI32EN4XA2KPP445XIBTMTRBLYWFIL556RDTHS2OWFQ2Z"
|
|
34
|
+
get_fee_payer = None # type: ignore
|
|
35
|
+
|
|
22
36
|
# Stellar Mainnet
|
|
23
37
|
STELLAR = NetworkConfig(
|
|
24
38
|
name="stellar",
|
|
@@ -127,3 +141,35 @@ def calculate_expiration_ledger(current_ledger: int, validity_ledgers: int = 60)
|
|
|
127
141
|
Expiration ledger number
|
|
128
142
|
"""
|
|
129
143
|
return current_ledger + validity_ledgers
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_stellar_fee_payer(network_name: str = "stellar") -> str:
|
|
147
|
+
"""
|
|
148
|
+
Get the fee payer address for a Stellar network.
|
|
149
|
+
|
|
150
|
+
The fee payer is the facilitator address that pays XLM transaction fees.
|
|
151
|
+
This address wraps the SorobanAuthorizationEntry in a fee-bump transaction.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
network_name: Network name ('stellar' or 'stellar-testnet')
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Fee payer public key (G... address) for the specified network
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
>>> get_stellar_fee_payer("stellar")
|
|
161
|
+
'GCHPGXJT2WFFRFCA5TV4G4E3PMMXLNIDUH27PKDYA4QJ2XGYZWGFZNHB'
|
|
162
|
+
>>> get_stellar_fee_payer("stellar-testnet")
|
|
163
|
+
'GBBFZMLUJEZVI32EN4XA2KPP445XIBTMTRBLYWFIL556RDTHS2OWFQ2Z'
|
|
164
|
+
"""
|
|
165
|
+
# Use facilitator module if available
|
|
166
|
+
if get_fee_payer is not None:
|
|
167
|
+
fee_payer = get_fee_payer(network_name)
|
|
168
|
+
if fee_payer:
|
|
169
|
+
return fee_payer
|
|
170
|
+
|
|
171
|
+
# Fallback to direct lookup
|
|
172
|
+
network_lower = network_name.lower()
|
|
173
|
+
if "testnet" in network_lower:
|
|
174
|
+
return STELLAR_FEE_PAYER_TESTNET
|
|
175
|
+
return STELLAR_FEE_PAYER_MAINNET
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: uvd-x402-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
uvd_x402_sdk/__init__.py,sha256=
|
|
1
|
+
uvd_x402_sdk/__init__.py,sha256=S_oA8b99W7CP6_RPtOlc7ZotYLvdTZJM9Sqc2s3O6zo,6261
|
|
2
2
|
uvd_x402_sdk/client.py,sha256=QbK22DtC3HmvvCezphQ-UsYX468vKrIN-M_wF4pv9cM,18389
|
|
3
3
|
uvd_x402_sdk/config.py,sha256=BNGnX2RwZ_ELIcSKU7RkwTUcln4LMFZdCwG1ptASKN8,8644
|
|
4
4
|
uvd_x402_sdk/decorators.py,sha256=XJ7V4554hsa-AVDrizF1oKmeTysg5zlkQRcaeGBI73E,9767
|
|
5
5
|
uvd_x402_sdk/exceptions.py,sha256=kzYNHFn41dSOZ5HaBDrbf4tdwJYQs1YcU9YybTrGZxo,6527
|
|
6
|
+
uvd_x402_sdk/facilitator.py,sha256=kX--Uk6BYlDCalKD-2LpPOgEi-t26C-sjcxgalphJp8,11859
|
|
6
7
|
uvd_x402_sdk/models.py,sha256=nfA8Ak0pyIveiNpstjqxnQ6-aPQ8TRJzJq2LJwlJc-E,14282
|
|
7
8
|
uvd_x402_sdk/response.py,sha256=4wxH4kWg1F8pokKEbN1kDvDF55cQxmQUN88HTxXht8g,13608
|
|
8
9
|
uvd_x402_sdk/integrations/__init__.py,sha256=Hq1Y0YIMYWBAtmbOLXDC40KQuCrbSpQVjAqEsbjH56s,1912
|
|
@@ -11,14 +12,14 @@ uvd_x402_sdk/integrations/fastapi_integration.py,sha256=j5h1IJwFLBBoWov7ANLCFaxe
|
|
|
11
12
|
uvd_x402_sdk/integrations/flask_integration.py,sha256=0iQKO5-WRxE76Pv-1jEl4lYhjCLmq_R-jxR5g9xIcKw,8825
|
|
12
13
|
uvd_x402_sdk/integrations/lambda_integration.py,sha256=nRf4o3nS6Syx-d5P0kEhz66y7jb_S4w-mwaIazgiA9c,10184
|
|
13
14
|
uvd_x402_sdk/networks/__init__.py,sha256=LKl_TljVoCDb27YB4X_VbQN8XKbdwWFAsCwgiqQtlgo,2092
|
|
14
|
-
uvd_x402_sdk/networks/algorand.py,sha256=
|
|
15
|
+
uvd_x402_sdk/networks/algorand.py,sha256=_eQfR8xguvTloJBR7_yGbWjC2KgmPux44pXeDVM7b0w,19458
|
|
15
16
|
uvd_x402_sdk/networks/base.py,sha256=gOPWfqasGbgtg9w2uG5pWnfjdOEain92L2egnDSBguc,14863
|
|
16
17
|
uvd_x402_sdk/networks/evm.py,sha256=4IbeaMH2I1c9DYCijghys0qYNeL2Nl92IMKLwq-b0Zg,10065
|
|
17
|
-
uvd_x402_sdk/networks/near.py,sha256=
|
|
18
|
-
uvd_x402_sdk/networks/solana.py,sha256
|
|
19
|
-
uvd_x402_sdk/networks/stellar.py,sha256=
|
|
20
|
-
uvd_x402_sdk-0.
|
|
21
|
-
uvd_x402_sdk-0.
|
|
22
|
-
uvd_x402_sdk-0.
|
|
23
|
-
uvd_x402_sdk-0.
|
|
24
|
-
uvd_x402_sdk-0.
|
|
18
|
+
uvd_x402_sdk/networks/near.py,sha256=HMMWJr-Jckj3YQTbSXkavlJWZB7ZV8OQX0bq8DuVxIg,12990
|
|
19
|
+
uvd_x402_sdk/networks/solana.py,sha256=GVUeh0O2W6f8Vbgoom2UQSGI8joZV68Pnpzhh87Bnqg,13640
|
|
20
|
+
uvd_x402_sdk/networks/stellar.py,sha256=ZuF-crx41N9MxSsgf2kAy88fsM-xvDyXY_D2ND6M71Y,5142
|
|
21
|
+
uvd_x402_sdk-0.5.0.dist-info/LICENSE,sha256=OcLzB_iSgMbvk7b0dlyvleY_IbL2WUaPxvn1CHw2uAc,1073
|
|
22
|
+
uvd_x402_sdk-0.5.0.dist-info/METADATA,sha256=2BkT_uVVDdWwuAnmjgS61pNXJWKP1e2wjWGf9R8VAEw,29711
|
|
23
|
+
uvd_x402_sdk-0.5.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
24
|
+
uvd_x402_sdk-0.5.0.dist-info/top_level.txt,sha256=Exyjj_Kl7CDAGFMi72lT9oFPOYiRNZb3l8tr906mMmc,13
|
|
25
|
+
uvd_x402_sdk-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|