uvd-x402-sdk 0.2.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 +169 -0
- uvd_x402_sdk/client.py +527 -0
- uvd_x402_sdk/config.py +249 -0
- uvd_x402_sdk/decorators.py +325 -0
- uvd_x402_sdk/exceptions.py +254 -0
- uvd_x402_sdk/integrations/__init__.py +74 -0
- uvd_x402_sdk/integrations/django_integration.py +237 -0
- uvd_x402_sdk/integrations/fastapi_integration.py +330 -0
- uvd_x402_sdk/integrations/flask_integration.py +259 -0
- uvd_x402_sdk/integrations/lambda_integration.py +320 -0
- uvd_x402_sdk/models.py +397 -0
- uvd_x402_sdk/networks/__init__.py +54 -0
- uvd_x402_sdk/networks/base.py +348 -0
- uvd_x402_sdk/networks/evm.py +235 -0
- uvd_x402_sdk/networks/near.py +397 -0
- uvd_x402_sdk/networks/solana.py +269 -0
- uvd_x402_sdk/networks/stellar.py +129 -0
- uvd_x402_sdk/response.py +439 -0
- uvd_x402_sdk-0.2.0.dist-info/LICENSE +21 -0
- uvd_x402_sdk-0.2.0.dist-info/METADATA +776 -0
- uvd_x402_sdk-0.2.0.dist-info/RECORD +23 -0
- uvd_x402_sdk-0.2.0.dist-info/WHEEL +5 -0
- uvd_x402_sdk-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NEAR Protocol network configuration.
|
|
3
|
+
|
|
4
|
+
NEAR uses NEP-366 meta-transactions for gasless payments:
|
|
5
|
+
1. User creates a DelegateAction containing ft_transfer
|
|
6
|
+
2. User signs to create SignedDelegateAction (Borsh serialized)
|
|
7
|
+
3. Facilitator wraps in Action::Delegate and submits
|
|
8
|
+
4. Facilitator pays all gas - user pays ZERO NEAR
|
|
9
|
+
|
|
10
|
+
NEP-366 Structure:
|
|
11
|
+
- DelegateAction: sender_id, receiver_id, actions, nonce, max_block_height, public_key
|
|
12
|
+
- SignedDelegateAction: delegate_action + ed25519 signature
|
|
13
|
+
- Hash prefix: 2^30 + 366 = 0x4000016E (little-endian)
|
|
14
|
+
|
|
15
|
+
Borsh Serialization Format:
|
|
16
|
+
- u8: 1 byte
|
|
17
|
+
- u32: 4 bytes little-endian
|
|
18
|
+
- u64: 8 bytes little-endian
|
|
19
|
+
- u128: 16 bytes little-endian
|
|
20
|
+
- string: u32 length prefix + UTF-8 bytes
|
|
21
|
+
- bytes: u32 length prefix + raw bytes
|
|
22
|
+
- fixed_bytes: raw bytes (no prefix)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import base64
|
|
26
|
+
import struct
|
|
27
|
+
from typing import Optional, Dict, Any
|
|
28
|
+
|
|
29
|
+
from uvd_x402_sdk.networks.base import (
|
|
30
|
+
NetworkConfig,
|
|
31
|
+
NetworkType,
|
|
32
|
+
register_network,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# NEP-366 hash prefix: (2^30 + 366) = 1073742190
|
|
36
|
+
NEP366_PREFIX = ((2**30) + 366).to_bytes(4, 'little')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# NEAR Mainnet
|
|
40
|
+
NEAR = NetworkConfig(
|
|
41
|
+
name="near",
|
|
42
|
+
display_name="NEAR Protocol",
|
|
43
|
+
network_type=NetworkType.NEAR,
|
|
44
|
+
chain_id=0, # Non-EVM, no chain ID
|
|
45
|
+
# Native Circle USDC on NEAR (account ID hash)
|
|
46
|
+
usdc_address="17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1",
|
|
47
|
+
usdc_decimals=6,
|
|
48
|
+
usdc_domain_name="", # Not applicable for NEAR
|
|
49
|
+
usdc_domain_version="",
|
|
50
|
+
rpc_url="https://rpc.mainnet.near.org",
|
|
51
|
+
enabled=True, # ENABLED: Facilitator supports NEAR via NEP-366
|
|
52
|
+
extra_config={
|
|
53
|
+
# Network identifier
|
|
54
|
+
"network_id": "mainnet",
|
|
55
|
+
# Alternative RPC endpoints (CORS-friendly)
|
|
56
|
+
"rpc_endpoints": [
|
|
57
|
+
"https://near.drpc.org",
|
|
58
|
+
"https://public-rpc.blockpi.io/http/near",
|
|
59
|
+
"https://endpoints.omniatech.io/v1/near/mainnet/public",
|
|
60
|
+
"https://rpc.mainnet.near.org",
|
|
61
|
+
"https://near.lava.build",
|
|
62
|
+
],
|
|
63
|
+
# Block explorer
|
|
64
|
+
"explorer_url": "https://nearblocks.io",
|
|
65
|
+
# NEP-366 prefix (2^30 + 366 as u32 little-endian)
|
|
66
|
+
"nep366_prefix": NEP366_PREFIX,
|
|
67
|
+
# Default gas for ft_transfer
|
|
68
|
+
"ft_transfer_gas": 30_000_000_000_000, # 30 TGas
|
|
69
|
+
# Deposit for ft_transfer (1 yoctoNEAR)
|
|
70
|
+
"ft_transfer_deposit": 1,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Register NEAR network
|
|
75
|
+
register_network(NEAR)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# =============================================================================
|
|
79
|
+
# NEAR-specific utilities
|
|
80
|
+
# =============================================================================
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_ft_transfer_args(
|
|
84
|
+
receiver_id: str,
|
|
85
|
+
amount: int,
|
|
86
|
+
memo: str = "",
|
|
87
|
+
) -> dict:
|
|
88
|
+
"""
|
|
89
|
+
Create arguments for NEAR ft_transfer function call.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
receiver_id: NEAR account ID to receive tokens
|
|
93
|
+
amount: Amount in base units (6 decimals for USDC)
|
|
94
|
+
memo: Optional transfer memo
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Dictionary of ft_transfer arguments
|
|
98
|
+
"""
|
|
99
|
+
return {
|
|
100
|
+
"receiver_id": receiver_id,
|
|
101
|
+
"amount": str(amount),
|
|
102
|
+
"memo": memo or None,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def calculate_max_block_height(current_height: int, blocks_valid: int = 1000) -> int:
|
|
107
|
+
"""
|
|
108
|
+
Calculate max_block_height for DelegateAction.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
current_height: Current block height from RPC
|
|
112
|
+
blocks_valid: Number of blocks the action is valid for (~17 minutes at 1 block/s)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Max block height for the action
|
|
116
|
+
"""
|
|
117
|
+
return current_height + blocks_valid
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def base58_decode(encoded: str) -> bytes:
|
|
121
|
+
"""
|
|
122
|
+
Decode a base58 string to bytes.
|
|
123
|
+
|
|
124
|
+
NEAR public keys use ed25519:base58 format.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
encoded: Base58 encoded string (without prefix)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Decoded bytes
|
|
131
|
+
"""
|
|
132
|
+
ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
133
|
+
BASE = 58
|
|
134
|
+
|
|
135
|
+
num = 0
|
|
136
|
+
for char in encoded:
|
|
137
|
+
index = ALPHABET.index(char)
|
|
138
|
+
if index == -1:
|
|
139
|
+
raise ValueError(f"Invalid base58 character: {char}")
|
|
140
|
+
num = num * BASE + index
|
|
141
|
+
|
|
142
|
+
# Convert to bytes (32 bytes for ED25519 public key)
|
|
143
|
+
result = []
|
|
144
|
+
while num > 0:
|
|
145
|
+
result.append(num & 0xFF)
|
|
146
|
+
num >>= 8
|
|
147
|
+
|
|
148
|
+
# Pad to 32 bytes and reverse
|
|
149
|
+
while len(result) < 32:
|
|
150
|
+
result.append(0)
|
|
151
|
+
|
|
152
|
+
return bytes(reversed(result))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def decode_near_public_key(public_key: str) -> bytes:
|
|
156
|
+
"""
|
|
157
|
+
Decode a NEAR public key from ed25519:base58 format to raw bytes.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
public_key: Public key in format 'ed25519:base58string'
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
32 bytes of the public key
|
|
164
|
+
"""
|
|
165
|
+
# Remove ed25519: prefix if present
|
|
166
|
+
if public_key.startswith("ed25519:"):
|
|
167
|
+
public_key = public_key[8:]
|
|
168
|
+
|
|
169
|
+
return base58_decode(public_key)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# =============================================================================
|
|
173
|
+
# NEP-366 Borsh Serialization
|
|
174
|
+
# =============================================================================
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class BorshSerializer:
|
|
178
|
+
"""
|
|
179
|
+
Simple Borsh serializer for NEAR transactions.
|
|
180
|
+
|
|
181
|
+
Borsh (Binary Object Representation Serializer for Hashing) is NEAR's
|
|
182
|
+
canonical binary serialization format. This implementation handles
|
|
183
|
+
the subset needed for NEP-366 meta-transactions.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(self) -> None:
|
|
187
|
+
self.buffer = bytearray()
|
|
188
|
+
|
|
189
|
+
def write_u8(self, value: int) -> "BorshSerializer":
|
|
190
|
+
"""Write unsigned 8-bit integer."""
|
|
191
|
+
self.buffer.extend(struct.pack('<B', value))
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def write_u32(self, value: int) -> "BorshSerializer":
|
|
195
|
+
"""Write unsigned 32-bit integer (little-endian)."""
|
|
196
|
+
self.buffer.extend(struct.pack('<I', value))
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
def write_u64(self, value: int) -> "BorshSerializer":
|
|
200
|
+
"""Write unsigned 64-bit integer (little-endian)."""
|
|
201
|
+
self.buffer.extend(struct.pack('<Q', value))
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
def write_u128(self, value: int) -> "BorshSerializer":
|
|
205
|
+
"""Write unsigned 128-bit integer (little-endian)."""
|
|
206
|
+
low = value & 0xFFFFFFFFFFFFFFFF
|
|
207
|
+
high = value >> 64
|
|
208
|
+
self.buffer.extend(struct.pack('<QQ', low, high))
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
def write_string(self, value: str) -> "BorshSerializer":
|
|
212
|
+
"""Write length-prefixed UTF-8 string."""
|
|
213
|
+
encoded = value.encode('utf-8')
|
|
214
|
+
self.write_u32(len(encoded))
|
|
215
|
+
self.buffer.extend(encoded)
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
def write_fixed_bytes(self, data: bytes) -> "BorshSerializer":
|
|
219
|
+
"""Write fixed-length bytes (no prefix)."""
|
|
220
|
+
self.buffer.extend(data)
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
def write_bytes(self, data: bytes) -> "BorshSerializer":
|
|
224
|
+
"""Write length-prefixed bytes."""
|
|
225
|
+
self.write_u32(len(data))
|
|
226
|
+
self.buffer.extend(data)
|
|
227
|
+
return self
|
|
228
|
+
|
|
229
|
+
def get_bytes(self) -> bytes:
|
|
230
|
+
"""Get the serialized bytes."""
|
|
231
|
+
return bytes(self.buffer)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def serialize_non_delegate_action(
|
|
235
|
+
receiver_id: str,
|
|
236
|
+
amount: int,
|
|
237
|
+
memo: Optional[str] = None,
|
|
238
|
+
) -> bytes:
|
|
239
|
+
"""
|
|
240
|
+
Serialize a NonDelegateAction for ft_transfer (NEP-366).
|
|
241
|
+
|
|
242
|
+
This creates the action that will be wrapped in a DelegateAction.
|
|
243
|
+
The action type is FunctionCall (type 2).
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
receiver_id: NEAR account ID to receive tokens
|
|
247
|
+
amount: Amount in raw units (6 decimals for USDC)
|
|
248
|
+
memo: Optional transfer memo
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Borsh-serialized action bytes
|
|
252
|
+
"""
|
|
253
|
+
import json
|
|
254
|
+
|
|
255
|
+
# Build ft_transfer args
|
|
256
|
+
args: Dict[str, Any] = {
|
|
257
|
+
"receiver_id": receiver_id,
|
|
258
|
+
"amount": str(amount),
|
|
259
|
+
}
|
|
260
|
+
if memo:
|
|
261
|
+
args["memo"] = memo
|
|
262
|
+
|
|
263
|
+
args_json = json.dumps(args, separators=(',', ':')).encode('utf-8')
|
|
264
|
+
|
|
265
|
+
ser = BorshSerializer()
|
|
266
|
+
ser.write_u8(2) # FunctionCall action type
|
|
267
|
+
ser.write_string("ft_transfer") # Method name
|
|
268
|
+
ser.write_bytes(args_json) # Args as JSON bytes
|
|
269
|
+
ser.write_u64(30_000_000_000_000) # 30 TGas
|
|
270
|
+
ser.write_u128(1) # 1 yoctoNEAR deposit (required for ft_transfer)
|
|
271
|
+
|
|
272
|
+
return ser.get_bytes()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def serialize_delegate_action(
|
|
276
|
+
sender_id: str,
|
|
277
|
+
receiver_id: str,
|
|
278
|
+
actions_bytes: bytes,
|
|
279
|
+
nonce: int,
|
|
280
|
+
max_block_height: int,
|
|
281
|
+
public_key_bytes: bytes,
|
|
282
|
+
) -> bytes:
|
|
283
|
+
"""
|
|
284
|
+
Serialize a DelegateAction for NEP-366 meta-transactions.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
sender_id: NEAR account ID of the sender
|
|
288
|
+
receiver_id: NEAR contract ID (USDC contract)
|
|
289
|
+
actions_bytes: Borsh-serialized NonDelegateAction
|
|
290
|
+
nonce: Access key nonce + 1
|
|
291
|
+
max_block_height: Block height when action expires
|
|
292
|
+
public_key_bytes: 32-byte ED25519 public key
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Borsh-serialized DelegateAction bytes
|
|
296
|
+
"""
|
|
297
|
+
ser = BorshSerializer()
|
|
298
|
+
ser.write_string(sender_id)
|
|
299
|
+
ser.write_string(receiver_id)
|
|
300
|
+
ser.write_u32(1) # 1 action
|
|
301
|
+
ser.write_fixed_bytes(actions_bytes)
|
|
302
|
+
ser.write_u64(nonce)
|
|
303
|
+
ser.write_u64(max_block_height)
|
|
304
|
+
ser.write_u8(0) # ED25519 key type
|
|
305
|
+
ser.write_fixed_bytes(public_key_bytes)
|
|
306
|
+
|
|
307
|
+
return ser.get_bytes()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def serialize_signed_delegate_action(
|
|
311
|
+
delegate_action_bytes: bytes,
|
|
312
|
+
signature_bytes: bytes,
|
|
313
|
+
) -> bytes:
|
|
314
|
+
"""
|
|
315
|
+
Serialize a SignedDelegateAction for NEP-366.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
delegate_action_bytes: Borsh-serialized DelegateAction
|
|
319
|
+
signature_bytes: 64-byte ED25519 signature
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Borsh-serialized SignedDelegateAction bytes
|
|
323
|
+
"""
|
|
324
|
+
ser = BorshSerializer()
|
|
325
|
+
ser.write_fixed_bytes(delegate_action_bytes)
|
|
326
|
+
ser.write_u8(0) # ED25519 signature type
|
|
327
|
+
ser.write_fixed_bytes(signature_bytes)
|
|
328
|
+
|
|
329
|
+
return ser.get_bytes()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# =============================================================================
|
|
333
|
+
# NEP-366 Payload Validation
|
|
334
|
+
# =============================================================================
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def validate_near_payload(payload: Dict[str, Any]) -> bool:
|
|
338
|
+
"""
|
|
339
|
+
Validate a NEAR payment payload structure.
|
|
340
|
+
|
|
341
|
+
The payload must contain a base64-encoded SignedDelegateAction.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
payload: Payload dictionary from x402 payment
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
True if valid, raises ValueError if invalid
|
|
348
|
+
"""
|
|
349
|
+
if "signedDelegateAction" not in payload:
|
|
350
|
+
raise ValueError("NEAR payload missing 'signedDelegateAction' field")
|
|
351
|
+
|
|
352
|
+
signed_delegate_b64 = payload["signedDelegateAction"]
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
# Decode base64
|
|
356
|
+
signed_delegate_bytes = base64.b64decode(signed_delegate_b64)
|
|
357
|
+
except Exception as e:
|
|
358
|
+
raise ValueError(f"Invalid base64 in signedDelegateAction: {e}")
|
|
359
|
+
|
|
360
|
+
# Basic length validation
|
|
361
|
+
# Minimum: sender_id (4 + 1) + receiver_id (4 + 1) + actions_count (4) +
|
|
362
|
+
# action (at least 1) + nonce (8) + max_block_height (8) +
|
|
363
|
+
# key_type (1) + public_key (32) + sig_type (1) + signature (64)
|
|
364
|
+
min_length = 4 + 1 + 4 + 1 + 4 + 1 + 8 + 8 + 1 + 32 + 1 + 64
|
|
365
|
+
if len(signed_delegate_bytes) < min_length:
|
|
366
|
+
raise ValueError(
|
|
367
|
+
f"SignedDelegateAction too short: {len(signed_delegate_bytes)} bytes, "
|
|
368
|
+
f"minimum {min_length} bytes"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def is_valid_near_account_id(account_id: str) -> bool:
|
|
375
|
+
"""
|
|
376
|
+
Validate a NEAR account ID format.
|
|
377
|
+
|
|
378
|
+
NEAR account IDs:
|
|
379
|
+
- Are 2-64 characters
|
|
380
|
+
- Contain only a-z, 0-9, _, -, .
|
|
381
|
+
- Cannot start with _ or -
|
|
382
|
+
- Cannot end with .
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
account_id: NEAR account ID to validate
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
True if valid format
|
|
389
|
+
"""
|
|
390
|
+
if not account_id or len(account_id) < 2 or len(account_id) > 64:
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
if account_id.startswith(('_', '-')) or account_id.endswith('.'):
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
allowed = set('abcdefghijklmnopqrstuvwxyz0123456789_-.')
|
|
397
|
+
return all(c in allowed for c in account_id)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solana Virtual Machine (SVM) network configurations.
|
|
3
|
+
|
|
4
|
+
This module supports all SVM-compatible chains:
|
|
5
|
+
- Solana (mainnet)
|
|
6
|
+
- Fogo (fast finality SVM)
|
|
7
|
+
|
|
8
|
+
All SVM chains use the same payment flow:
|
|
9
|
+
1. User creates a partially-signed VersionedTransaction
|
|
10
|
+
2. Transaction contains SPL token TransferChecked instruction
|
|
11
|
+
3. Facilitator is fee payer (user pays ZERO SOL/tokens)
|
|
12
|
+
4. Facilitator co-signs and submits transaction
|
|
13
|
+
|
|
14
|
+
Transaction Structure (REQUIRED by facilitator):
|
|
15
|
+
- Instruction 0: SetComputeUnitLimit (units: 20,000)
|
|
16
|
+
- Instruction 1: SetComputeUnitPrice (microLamports: 1)
|
|
17
|
+
- Instruction 2: TransferChecked (USDC transfer)
|
|
18
|
+
|
|
19
|
+
The facilitator validates this exact structure in src/chain/solana.rs.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import base64
|
|
23
|
+
from typing import Dict, Any, Optional
|
|
24
|
+
|
|
25
|
+
from uvd_x402_sdk.networks.base import (
|
|
26
|
+
NetworkConfig,
|
|
27
|
+
NetworkType,
|
|
28
|
+
register_network,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# =============================================================================
|
|
33
|
+
# SVM Networks Configuration
|
|
34
|
+
# =============================================================================
|
|
35
|
+
|
|
36
|
+
# Solana Mainnet
|
|
37
|
+
SOLANA = NetworkConfig(
|
|
38
|
+
name="solana",
|
|
39
|
+
display_name="Solana",
|
|
40
|
+
network_type=NetworkType.SVM, # Use SVM type for all Solana-compatible chains
|
|
41
|
+
chain_id=0, # Non-EVM, no chain ID
|
|
42
|
+
usdc_address="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", # USDC SPL token mint
|
|
43
|
+
usdc_decimals=6,
|
|
44
|
+
usdc_domain_name="", # Not applicable for SVM
|
|
45
|
+
usdc_domain_version="",
|
|
46
|
+
rpc_url="https://api.mainnet-beta.solana.com",
|
|
47
|
+
enabled=True,
|
|
48
|
+
extra_config={
|
|
49
|
+
# Token program ID (standard SPL token program)
|
|
50
|
+
"token_program_id": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
|
51
|
+
# Associated Token Account program
|
|
52
|
+
"ata_program_id": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
|
|
53
|
+
# Default compute units for transfer
|
|
54
|
+
"compute_units": 20000,
|
|
55
|
+
# Default priority fee in microLamports
|
|
56
|
+
"priority_fee_microlamports": 1,
|
|
57
|
+
# Block explorer
|
|
58
|
+
"explorer_url": "https://solscan.io",
|
|
59
|
+
# Genesis hash (first 32 chars for CAIP-2)
|
|
60
|
+
"genesis_hash": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
61
|
+
# Network type identifier
|
|
62
|
+
"svm_network": "solana",
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Fogo (SVM chain with ultra-fast finality)
|
|
67
|
+
FOGO = NetworkConfig(
|
|
68
|
+
name="fogo",
|
|
69
|
+
display_name="Fogo",
|
|
70
|
+
network_type=NetworkType.SVM,
|
|
71
|
+
chain_id=0, # Non-EVM, no chain ID
|
|
72
|
+
usdc_address="uSd2czE61Evaf76RNbq4KPpXnkiL3irdzgLFUMe3NoG", # Fogo USDC mint
|
|
73
|
+
usdc_decimals=6,
|
|
74
|
+
usdc_domain_name="", # Not applicable for SVM
|
|
75
|
+
usdc_domain_version="",
|
|
76
|
+
rpc_url="https://rpc.fogo.nightly.app/",
|
|
77
|
+
enabled=True,
|
|
78
|
+
extra_config={
|
|
79
|
+
# Token program ID (standard SPL token program - same as Solana)
|
|
80
|
+
"token_program_id": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
|
81
|
+
# Associated Token Account program (same as Solana)
|
|
82
|
+
"ata_program_id": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
|
|
83
|
+
# Default compute units for transfer
|
|
84
|
+
"compute_units": 20000,
|
|
85
|
+
# Default priority fee in microLamports
|
|
86
|
+
"priority_fee_microlamports": 1,
|
|
87
|
+
# Block explorer (placeholder - update when available)
|
|
88
|
+
"explorer_url": "https://explorer.fogo.nightly.app",
|
|
89
|
+
# Network type identifier
|
|
90
|
+
"svm_network": "fogo",
|
|
91
|
+
# Fogo-specific: Ultra-fast finality (~400ms)
|
|
92
|
+
"finality_ms": 400,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Register SVM networks
|
|
97
|
+
register_network(SOLANA)
|
|
98
|
+
register_network(FOGO)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# =============================================================================
|
|
102
|
+
# SVM-specific utilities
|
|
103
|
+
# =============================================================================
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def is_svm_network(network_name: str) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Check if a network is SVM-compatible.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
network_name: Network name to check
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if network uses SVM (Solana, Fogo, etc.)
|
|
115
|
+
"""
|
|
116
|
+
from uvd_x402_sdk.networks.base import get_network, NetworkType
|
|
117
|
+
|
|
118
|
+
network = get_network(network_name)
|
|
119
|
+
if not network:
|
|
120
|
+
return False
|
|
121
|
+
return NetworkType.is_svm(network.network_type)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_svm_networks() -> list:
|
|
125
|
+
"""
|
|
126
|
+
Get all registered SVM networks.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of SVM NetworkConfig instances
|
|
130
|
+
"""
|
|
131
|
+
from uvd_x402_sdk.networks.base import list_networks, NetworkType
|
|
132
|
+
|
|
133
|
+
return [
|
|
134
|
+
n for n in list_networks(enabled_only=True)
|
|
135
|
+
if NetworkType.is_svm(n.network_type)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_associated_token_address(owner: str, mint: str) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Derive the Associated Token Account (ATA) address for an owner and mint.
|
|
142
|
+
|
|
143
|
+
Note: This is a placeholder. For actual derivation, use the solana-py library:
|
|
144
|
+
|
|
145
|
+
from solders.pubkey import Pubkey
|
|
146
|
+
from spl.token.constants import TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID
|
|
147
|
+
from spl.token.instructions import get_associated_token_address
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
owner: Owner's public key (base58)
|
|
151
|
+
mint: Token mint address (base58)
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Associated token account address (base58)
|
|
155
|
+
"""
|
|
156
|
+
# This would require solders/solana-py for actual implementation
|
|
157
|
+
# Returning empty string as placeholder
|
|
158
|
+
raise NotImplementedError(
|
|
159
|
+
"ATA derivation requires solana-py library. "
|
|
160
|
+
"Install with: pip install uvd-x402-sdk[solana]"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def validate_svm_transaction_structure(transaction_base64: str) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Validate that an SVM transaction has the correct structure for x402.
|
|
167
|
+
|
|
168
|
+
The facilitator expects:
|
|
169
|
+
- VersionedTransaction with exactly 3 instructions
|
|
170
|
+
- Instruction 0: SetComputeUnitLimit
|
|
171
|
+
- Instruction 1: SetComputeUnitPrice
|
|
172
|
+
- Instruction 2: TransferChecked (SPL token)
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
transaction_base64: Base64-encoded serialized transaction
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if structure is valid
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
ValueError: If structure is invalid
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
tx_bytes = base64.b64decode(transaction_base64)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise ValueError(f"Invalid base64 transaction: {e}")
|
|
187
|
+
|
|
188
|
+
# Basic length validation
|
|
189
|
+
# Minimum: version (1) + header (3) + accounts array (varies) + blockhash (32) + instructions
|
|
190
|
+
if len(tx_bytes) < 50:
|
|
191
|
+
raise ValueError(f"Transaction too short: {len(tx_bytes)} bytes")
|
|
192
|
+
|
|
193
|
+
# Full validation requires solders/solana-py
|
|
194
|
+
# For now, we just check basic structure
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def validate_svm_payload(payload: Dict[str, Any]) -> bool:
|
|
199
|
+
"""
|
|
200
|
+
Validate an SVM payment payload structure.
|
|
201
|
+
|
|
202
|
+
The payload must contain a base64-encoded partially-signed transaction.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
payload: Payload dictionary from x402 payment
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if valid, raises ValueError if invalid
|
|
209
|
+
"""
|
|
210
|
+
if "transaction" not in payload:
|
|
211
|
+
raise ValueError("SVM payload missing 'transaction' field")
|
|
212
|
+
|
|
213
|
+
transaction_b64 = payload["transaction"]
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
tx_bytes = base64.b64decode(transaction_b64)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
raise ValueError(f"Invalid base64 in transaction: {e}")
|
|
219
|
+
|
|
220
|
+
# Basic length validation
|
|
221
|
+
if len(tx_bytes) < 50:
|
|
222
|
+
raise ValueError(f"Transaction too short: {len(tx_bytes)} bytes")
|
|
223
|
+
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def is_valid_solana_address(address: str) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Validate a Solana/SVM public key format.
|
|
230
|
+
|
|
231
|
+
Solana addresses are base58-encoded 32-byte public keys.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
address: Address to validate
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
True if valid base58 address
|
|
238
|
+
"""
|
|
239
|
+
if not address or not isinstance(address, str):
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
# Base58 alphabet (no 0, O, I, l)
|
|
243
|
+
base58_alphabet = set("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
|
|
244
|
+
|
|
245
|
+
if not all(c in base58_alphabet for c in address):
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
# Typical Solana address length is 32-44 characters
|
|
249
|
+
return 32 <= len(address) <= 44
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# =============================================================================
|
|
253
|
+
# Transaction Building Utilities (for reference)
|
|
254
|
+
# =============================================================================
|
|
255
|
+
|
|
256
|
+
# These constants are useful for building transactions programmatically
|
|
257
|
+
|
|
258
|
+
# Compute Budget Program
|
|
259
|
+
COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111"
|
|
260
|
+
|
|
261
|
+
# SetComputeUnitLimit instruction discriminator
|
|
262
|
+
SET_COMPUTE_UNIT_LIMIT_DISCRIMINATOR = 2
|
|
263
|
+
|
|
264
|
+
# SetComputeUnitPrice instruction discriminator
|
|
265
|
+
SET_COMPUTE_UNIT_PRICE_DISCRIMINATOR = 3
|
|
266
|
+
|
|
267
|
+
# Default values for x402 transactions
|
|
268
|
+
DEFAULT_COMPUTE_UNITS = 20000
|
|
269
|
+
DEFAULT_PRIORITY_FEE_MICROLAMPORTS = 1
|