uvd-x402-sdk 0.2.2__py3-none-any.whl → 0.2.3__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 -169
- uvd_x402_sdk/client.py +527 -527
- uvd_x402_sdk/config.py +248 -248
- uvd_x402_sdk/decorators.py +325 -325
- uvd_x402_sdk/exceptions.py +254 -254
- uvd_x402_sdk/integrations/__init__.py +74 -74
- uvd_x402_sdk/integrations/django_integration.py +237 -237
- uvd_x402_sdk/integrations/fastapi_integration.py +330 -330
- uvd_x402_sdk/integrations/flask_integration.py +259 -259
- uvd_x402_sdk/integrations/lambda_integration.py +320 -320
- uvd_x402_sdk/models.py +397 -397
- uvd_x402_sdk/networks/__init__.py +54 -54
- uvd_x402_sdk/networks/base.py +347 -347
- uvd_x402_sdk/networks/evm.py +215 -215
- uvd_x402_sdk/networks/near.py +397 -397
- uvd_x402_sdk/networks/solana.py +282 -282
- uvd_x402_sdk/networks/stellar.py +129 -129
- uvd_x402_sdk/response.py +439 -439
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.2.3.dist-info}/LICENSE +21 -21
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.2.3.dist-info}/METADATA +814 -778
- uvd_x402_sdk-0.2.3.dist-info/RECORD +23 -0
- uvd_x402_sdk-0.2.2.dist-info/RECORD +0 -23
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.2.3.dist-info}/WHEEL +0 -0
- {uvd_x402_sdk-0.2.2.dist-info → uvd_x402_sdk-0.2.3.dist-info}/top_level.txt +0 -0
uvd_x402_sdk/networks/near.py
CHANGED
|
@@ -1,397 +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)
|
|
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)
|