uvd-x402-sdk 0.2.1__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.
@@ -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)