@monadns/sdk 2.1.0 → 2.3.0

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @monadns/sdk
2
2
 
3
- Resolve and register **.mon** names on the Monad network. The official SDK for the [Monad Name Service](https://monadscan.com).
3
+ Resolve and register **.mon** names on the Monad network. The official SDK for the [Monad Name Service](https://monadscan.com/address/0x98866c55adbc73ec6c272bb3604ddbdee3f282a8#readContract).
4
4
 
5
5
  ## Install
6
6
 
@@ -144,7 +144,11 @@ import { clearCache } from '@monadns/sdk';
144
144
  function RegisterPage() {
145
145
  const [label, setLabel] = useState('');
146
146
  const { address } = useAccount();
147
- const { info, loading: checking } = useRegistrationInfo(label, address);
147
+ const {
148
+ info,
149
+ loading: checking,
150
+ error: checkError,
151
+ } = useRegistrationInfo(label, address);
148
152
  const { prepare, loading: preparing, error } = useMNSRegister();
149
153
  const { writeContract, data: hash } = useWriteContract();
150
154
  const { isLoading: confirming, isSuccess } = useWaitForTransactionReceipt({
@@ -171,8 +175,9 @@ function RegisterPage() {
171
175
  />
172
176
 
173
177
  {checking && <p className="text-gray-400">Checking availability...</p>}
178
+ {checkError && <p className="text-red-500">❌ {checkError}</p>}
174
179
 
175
- {info && !checking && (
180
+ {info && !checking && !checkError && (
176
181
  <div className="space-y-2">
177
182
  <p>{info.available ? '✅ Available!' : '❌ Already taken'}</p>
178
183
 
@@ -303,6 +308,152 @@ function ProfileEditor({ name }: { name: string }) {
303
308
  }
304
309
  ```
305
310
 
311
+ ## Name Validation
312
+
313
+ The SDK automatically validates names and provides clear error messages:
314
+
315
+ ```typescript
316
+ import { validateMonName } from '@monadns/sdk';
317
+
318
+ const validation = validateMonName('alice');
319
+ if (!validation.valid) {
320
+ console.error(validation.error);
321
+ }
322
+
323
+ // Invalid examples that are rejected:
324
+ validateMonName('0x123'); // ❌ "Name cannot start with "0x" (reserved for addresses)"
325
+ validateMonName('hello--world'); // ❌ "Name cannot contain consecutive hyphens"
326
+ validateMonName('-alice'); // ❌ "Name can only contain lowercase letters..."
327
+ validateMonName('a'.repeat(64)); // ❌ "Name must be 63 characters or less"
328
+ validateMonName(''); // ❌ "Name cannot be empty"
329
+
330
+ // Valid examples:
331
+ validateMonName('alice'); // ✅
332
+ validateMonName('a'); // ✅ 1-character names allowed
333
+ validateMonName('ab'); // ✅ 2-character names allowed
334
+ validateMonName('web3-name'); // ✅ Hyphens allowed (not at start/end)
335
+ ```
336
+
337
+ ## Avatar Best Practices
338
+
339
+ ### Recommended Workflow (Enforced by SDK)
340
+
341
+ ```tsx
342
+ import {
343
+ validateAvatarFull,
344
+ useMNSTextRecords,
345
+ MAX_AVATAR_BYTES,
346
+ } from '@monadns/sdk/react';
347
+ import { useWriteContract } from 'wagmi';
348
+
349
+ function AvatarUploader({ name }: { name: string }) {
350
+ const [error, setError] = useState('');
351
+ const { setAvatarValidated } = useMNSTextRecords();
352
+ const { writeContract } = useWriteContract();
353
+
354
+ const handleUpload = async (url: string) => {
355
+ try {
356
+ setError('');
357
+
358
+ // SDK validates size automatically (throws if > 50KB)
359
+ const tx = await setAvatarValidated(name, url);
360
+ writeContract(tx);
361
+ } catch (e: any) {
362
+ setError(e.message);
363
+ // "Avatar file size (125.3KB) exceeds 50KB limit. Please optimize..."
364
+ }
365
+ };
366
+
367
+ return (
368
+ <div>
369
+ <input onChange={(e) => handleUpload(e.target.value)} />
370
+ {error && <p className="text-red-500">{error}</p>}
371
+ <p className="text-sm text-gray-500">
372
+ Max size: {(MAX_AVATAR_BYTES / 1024).toFixed(0)}KB (WebP/SVG
373
+ recommended)
374
+ </p>
375
+ </div>
376
+ );
377
+ }
378
+ ```
379
+
380
+ ### Size Limits (Automatically Enforced)
381
+
382
+ - **Data URIs**: Strict 50KB limit (enforced immediately)
383
+ - **Remote URLs** (IPFS/HTTP): Validated when using `setAvatarValidated()`
384
+ - **NFT avatars**: No size check (resolved at display time)
385
+
386
+ ### Supported Avatar Formats
387
+
388
+ ```typescript
389
+ // ✅ HTTPS URLs
390
+ setAvatar('alice.mon', 'https://example.com/avatar.png');
391
+
392
+ // ✅ IPFS with protocol
393
+ setAvatar('alice.mon', 'ipfs://QmX...');
394
+
395
+ // ✅ Raw IPFS CID (auto-converted)
396
+ setAvatar('alice.mon', 'QmX...'); // → ipfs://QmX...
397
+
398
+ // ✅ Arweave
399
+ setAvatar('alice.mon', 'ar://abc123...');
400
+
401
+ // ✅ NFT avatar
402
+ setAvatar('alice.mon', 'eip155:1/erc721:0x.../123');
403
+
404
+ // ✅ Data URI (with size check)
405
+ setAvatar('alice.mon', 'data:image/svg+xml;base64,...');
406
+ ```
407
+
408
+ ### Manual Validation (Advanced)
409
+
410
+ ```typescript
411
+ import { validateAvatarUri, validateAvatarFull } from '@monadns/sdk';
412
+
413
+ // Quick format check (synchronous)
414
+ const validation = validateAvatarUri(avatarUrl);
415
+ if (!validation.valid) {
416
+ console.error(validation.error);
417
+ }
418
+
419
+ // Full validation including size check (async)
420
+ try {
421
+ await validateAvatarFull(avatarUrl);
422
+ // Safe to use
423
+ } catch (error) {
424
+ console.error(error.message);
425
+ // "Avatar file size (65.3KB) exceeds 50KB limit..."
426
+ }
427
+
428
+ // Check remote file size
429
+ import { getRemoteAvatarSize, MAX_AVATAR_BYTES } from '@monadns/sdk';
430
+
431
+ const size = await getRemoteAvatarSize('https://example.com/avatar.png');
432
+ if (size && size > MAX_AVATAR_BYTES) {
433
+ alert(`Avatar too large: ${(size / 1024).toFixed(1)}KB. Max is 50KB.`);
434
+ }
435
+ ```
436
+
437
+ ### Fallback Avatars
438
+
439
+ ```typescript
440
+ import { getAvatarUrl, DEFAULT_AVATAR_PLACEHOLDER } from '@monadns/sdk';
441
+
442
+ // Use fallback if avatar not set or fails to resolve
443
+ const avatar = await getAvatarUrl('alice.mon', undefined, {
444
+ fallback: DEFAULT_AVATAR_PLACEHOLDER,
445
+ });
446
+ ```
447
+
448
+ ### Override Validation (Not Recommended)
449
+
450
+ If you need to bypass validation (not recommended for production):
451
+
452
+ ```typescript
453
+ // Use basic setAvatar() - only validates data URIs
454
+ const tx = setAvatar(name, largeAvatarUrl); // Logs warning
455
+ ```
456
+
306
457
  ## Advanced Usage
307
458
 
308
459
  ### Check Name Ownership & Expiry
@@ -351,7 +502,7 @@ if (available) {
351
502
  | `getDisplayName(address)` | Returns `.mon` name or truncated address |
352
503
  | `resolveInput(input)` | Accepts name or address, returns address |
353
504
  | `getTextRecord(name, key)` | Get text record (avatar, url, etc.) |
354
- | `getAvatarUrl(name)` | Get resolved avatar URL (handles IPFS, NFTs) |
505
+ | `getAvatarUrl(name, config, options)` | Get resolved avatar URL (handles IPFS, NFTs) |
355
506
  | `getAvailable(name)` | Check if a name is available for registration |
356
507
  | `getOwner(name)` | Get the owner address of a name |
357
508
  | `getResolver(name)` | Get the resolver contract address |
@@ -361,6 +512,10 @@ if (available) {
361
512
  | `getRegisterTx(label, address)` | Get tx params for registration |
362
513
  | `getSetTextTx(name, key, value)` | Get tx params for setting a text record |
363
514
  | `getSetAddrTx(name, address)` | Get tx params for updating the address record |
515
+ | `validateMonName(label)` | Validate .mon name format and rules |
516
+ | `validateAvatarUri(uri)` | Validate avatar format and data URI size |
517
+ | `validateAvatarFull(uri)` | Async validation including remote file size |
518
+ | `getRemoteAvatarSize(url)` | Get file size of remote URL (helper for UI) |
364
519
 
365
520
  ### React Hooks (`@monadns/sdk/react`)
366
521
 
@@ -377,9 +532,20 @@ if (available) {
377
532
  | `useMNSExpiry(name)` | `{ expiry, expiryDate, daysUntilExpiry, isExpired, loading, error }` | Get expiration info |
378
533
  | `useRegistrationInfo(label, address)` | `{ info, loading, error }` | Pre-registration data |
379
534
  | `useMNSRegister()` | `{ prepare, tx, loading, error }` | Prepare registration tx |
380
- | `useMNSTextRecords()` | `{ setAvatar, setTwitter, ... }` | Prepare text record txs |
535
+ | `useMNSTextRecords()` | `{ setAvatar, setAvatarValidated, setTwitter, ... }` | Prepare text record txs |
381
536
  | `useMNSAddr()` | `{ setAddr }` | Prepare address update tx |
382
537
 
538
+ ### Constants
539
+
540
+ | Constant | Value |
541
+ | ---------------------------- | -------------------------------------------- |
542
+ | `MAX_AVATAR_BYTES` | `51200` (50KB) |
543
+ | `DEFAULT_AVATAR_PLACEHOLDER` | Transparent 1x1 SVG data URI |
544
+ | `MNS_REGISTRY` | `0x13f963486e741c8d3fcdc0a34a910920339a19ce` |
545
+ | `MNS_PUBLIC_RESOLVER` | `0xa2eb94c88e55d944aced2066c5cec9b759801f97` |
546
+ | `MNS_CONTROLLER` | `0x98866c55adbc73ec6c272bb3604ddbdee3f282a8` |
547
+ | `MNS_BASE_REGISTRAR` | `0x104a49db9318c284d462841b6940bdb46624ca55` |
548
+
383
549
  ### Custom RPC
384
550
 
385
551
  All functions accept an optional config object:
@@ -412,33 +578,43 @@ const name = await lookupAddress('0x...', { client });
412
578
 
413
579
  **Chain:** Monad Mainnet (Chain ID 143)
414
580
 
415
- ## Migrating from v2.0 to v2.1
581
+ ## Changelog
416
582
 
417
- ### Breaking Changes
583
+ ### v2.3.0
418
584
 
419
- **`isRegistered()` has been renamed to `getAvailable()` with inverted logic**
585
+ **New Features:**
420
586
 
421
- **Before (v2.0):**
587
+ - **Name validation**: Added `validateMonName()` function with comprehensive validation rules
588
+ - **1 & 2-character names**: Removed artificial 3-character minimum - all lengths now supported
589
+ - **0x prefix protection**: Names cannot start with "0x" to prevent confusion with addresses
590
+ - **Automatic validation**: `useRegistrationInfo()` and write functions now validate names automatically
422
591
 
423
- ```typescript
424
- const registered = await isRegistered('alice.mon');
425
- if (registered) {
426
- console.log('Name is taken');
427
- }
428
- ```
592
+ **Improvements:**
429
593
 
430
- **After (v2.1):**
594
+ - Better error messages for invalid names
595
+ - Validation happens client-side before RPC calls (faster feedback)
596
+ - Consistent validation across all entry points
431
597
 
432
- ```typescript
433
- const available = await getAvailable('alice.mon');
434
- if (available) {
435
- console.log('Name is available for registration');
436
- } else {
437
- console.log('Name is taken');
438
- }
439
- ```
598
+ ### v2.2.0
599
+
600
+ **New Features:**
601
+
602
+ - **Avatar validation**: `validateAvatarUri()` and `validateAvatarFull()` for size checking
603
+ - **Avatar constants**: `MAX_AVATAR_BYTES` (50KB limit) and `DEFAULT_AVATAR_PLACEHOLDER`
604
+ - **Enhanced avatar resolution**: Support for raw IPFS CIDs and Arweave URIs
605
+ - **Validated avatar setter**: `setAvatarValidated()` hook with automatic size validation
606
+ - **Size helper**: `getRemoteAvatarSize()` for checking remote file sizes
607
+ - **Fallback support**: `getAvatarUrl()` now accepts fallback option
608
+
609
+ **Behavior Changes:**
440
610
 
441
- ### New Features
611
+ - Data URI avatars are now strictly limited to 50KB (throws error if exceeded)
612
+ - Setting non-data-URI avatars without validation logs a warning
613
+ - `getAvatarUrl()` now handles raw IPFS CIDs (e.g., `QmX...`) automatically
614
+
615
+ ### v2.1.0
616
+
617
+ **New Features:**
442
618
 
443
619
  - `getOwner(name)` - Get the owner of a name
444
620
  - `getResolver(name)` - Get the resolver contract
@@ -448,6 +624,10 @@ if (available) {
448
624
  - `useMNSExpiry(name)` - React hook for expiry info with computed fields
449
625
  - `MNS_BASE_REGISTRAR` constant now exported
450
626
 
627
+ **Breaking Changes:**
628
+
629
+ - `isRegistered()` → `getAvailable()` (inverted boolean logic)
630
+
451
631
  ## Compatibility
452
632
 
453
633
  - **Frameworks:** React 17+, Next.js (App Router & Pages), Remix, Vite, CRA
package/dist/index.cjs CHANGED
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ DEFAULT_AVATAR_PLACEHOLDER: () => DEFAULT_AVATAR_PLACEHOLDER,
24
+ MAX_AVATAR_BYTES: () => MAX_AVATAR_BYTES,
23
25
  MNS_BASE_REGISTRAR: () => MNS_BASE_REGISTRAR,
24
26
  MNS_CONTROLLER: () => MNS_CONTROLLER,
25
27
  MNS_PUBLIC_RESOLVER: () => MNS_PUBLIC_RESOLVER,
@@ -36,6 +38,7 @@ __export(src_exports, {
36
38
  getOwner: () => getOwner,
37
39
  getRegisterTx: () => getRegisterTx,
38
40
  getRegistrationInfo: () => getRegistrationInfo,
41
+ getRemoteAvatarSize: () => getRemoteAvatarSize,
39
42
  getResolver: () => getResolver,
40
43
  getSetAddrTx: () => getSetAddrTx,
41
44
  getSetTextTx: () => getSetTextTx,
@@ -43,7 +46,10 @@ __export(src_exports, {
43
46
  lookupAddress: () => lookupAddress,
44
47
  resolveInput: () => resolveInput,
45
48
  resolveName: () => resolveName,
46
- resolverWriteAbi: () => resolverWriteAbi
49
+ resolverWriteAbi: () => resolverWriteAbi,
50
+ validateAvatarFull: () => validateAvatarFull,
51
+ validateAvatarUri: () => validateAvatarUri,
52
+ validateMonName: () => validateMonName
47
53
  });
48
54
  module.exports = __toCommonJS(src_exports);
49
55
 
@@ -77,6 +83,8 @@ var MNS_PUBLIC_RESOLVER = "0xa2eb94c88e55d944aced2066c5cec9b759801f97";
77
83
  var MNS_CONTROLLER = "0x98866c55adbc73ec6c272bb3604ddbdee3f282a8";
78
84
  var MNS_BASE_REGISTRAR = "0x104a49db9318c284d462841b6940bdb46624ca55";
79
85
  var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
86
+ var MAX_AVATAR_BYTES = 51200;
87
+ var DEFAULT_AVATAR_PLACEHOLDER = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"%3E%3C/svg%3E';
80
88
  var registryAbi = [
81
89
  {
82
90
  name: "resolver",
@@ -285,21 +293,28 @@ async function resolveInput(input, config) {
285
293
  if (trimmed.endsWith(".mon")) return resolveName(trimmed, config);
286
294
  return resolveName(`${trimmed}.mon`, config);
287
295
  }
288
- async function getAvatarUrl(name, config) {
296
+ async function getAvatarUrl(name, config, options) {
289
297
  const raw = await getTextRecord(name, "avatar", config);
290
- if (!raw) return null;
291
- if (raw.startsWith("http://") || raw.startsWith("https://")) {
292
- return raw;
293
- }
294
- if (raw.startsWith("ipfs://")) {
295
- const hash = raw.slice(7);
296
- return `https://ipfs.io/ipfs/${hash}`;
297
- }
298
- const nftMatch = raw.match(
299
- /^eip155:(\d+)\/(erc721|erc1155):0x([a-fA-F0-9]{40})\/(\d+)$/
300
- );
301
- if (nftMatch) {
302
- try {
298
+ if (!raw) return options?.fallback || null;
299
+ try {
300
+ if (raw.startsWith("http://") || raw.startsWith("https://")) {
301
+ return raw;
302
+ }
303
+ if (raw.startsWith("ipfs://")) {
304
+ const hash = raw.slice(7);
305
+ return `https://ipfs.io/ipfs/${hash}`;
306
+ }
307
+ if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[0-9A-Za-z]{50,})/.test(raw)) {
308
+ return `https://ipfs.io/ipfs/${raw}`;
309
+ }
310
+ if (raw.startsWith("ar://")) {
311
+ const hash = raw.slice(5);
312
+ return `https://arweave.net/${hash}`;
313
+ }
314
+ const nftMatch = raw.match(
315
+ /^eip155:(\d+)\/(erc721|erc1155):0x([a-fA-F0-9]{40})\/(\d+)$/
316
+ );
317
+ if (nftMatch) {
303
318
  const client = getMNSClient(config);
304
319
  const contract = `0x${nftMatch[3]}`;
305
320
  const tokenId = BigInt(nftMatch[4]);
@@ -327,15 +342,16 @@ async function getAvatarUrl(name, config) {
327
342
  if (image && image.startsWith("ipfs://")) {
328
343
  image = `https://ipfs.io/ipfs/${image.slice(7)}`;
329
344
  }
330
- return image;
331
- } catch {
332
- return null;
345
+ return image || options?.fallback || null;
333
346
  }
347
+ if (raw.startsWith("data:")) {
348
+ return raw;
349
+ }
350
+ return options?.fallback || null;
351
+ } catch (error) {
352
+ console.warn("Failed to resolve avatar:", error);
353
+ return options?.fallback || null;
334
354
  }
335
- if (raw.startsWith("data:")) {
336
- return raw;
337
- }
338
- return null;
339
355
  }
340
356
  function clearCache() {
341
357
  nameCache.clear();
@@ -344,6 +360,127 @@ function clearCache() {
344
360
 
345
361
  // src/write.ts
346
362
  var import_viem3 = require("viem");
363
+
364
+ // src/utils.ts
365
+ function validateMonName(label) {
366
+ if (!label || label.trim().length === 0) {
367
+ return { valid: false, error: "Name cannot be empty" };
368
+ }
369
+ const trimmed = label.trim().toLowerCase();
370
+ if (trimmed.startsWith("0x")) {
371
+ return {
372
+ valid: false,
373
+ error: 'Name cannot start with "0x" (reserved for addresses)'
374
+ };
375
+ }
376
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(trimmed)) {
377
+ return {
378
+ valid: false,
379
+ error: "Name can only contain lowercase letters, numbers, and hyphens (not at start/end)"
380
+ };
381
+ }
382
+ if (trimmed.includes("--")) {
383
+ return { valid: false, error: "Name cannot contain consecutive hyphens" };
384
+ }
385
+ if (trimmed.length > 63) {
386
+ return { valid: false, error: "Name must be 63 characters or less" };
387
+ }
388
+ return { valid: true };
389
+ }
390
+ function validateAvatarUri(uri) {
391
+ if (!uri) {
392
+ return { valid: false, error: "Avatar URI is required" };
393
+ }
394
+ if (uri.startsWith("data:")) {
395
+ const sizeBytes = new Blob([uri]).size;
396
+ if (sizeBytes > MAX_AVATAR_BYTES) {
397
+ return {
398
+ valid: false,
399
+ error: `Avatar size (${(sizeBytes / 1024).toFixed(1)}KB) exceeds ${MAX_AVATAR_BYTES / 1024}KB limit. Please optimize as WebP or SVG for better performance.`,
400
+ sizeBytes
401
+ };
402
+ }
403
+ return { valid: true, sizeBytes };
404
+ }
405
+ const validSchemes = ["http://", "https://", "ipfs://", "eip155:", "ar://"];
406
+ const hasValidScheme = validSchemes.some((scheme) => uri.startsWith(scheme));
407
+ if (!hasValidScheme) {
408
+ if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[0-9A-Za-z]{50,})/.test(uri)) {
409
+ return { valid: true };
410
+ }
411
+ return {
412
+ valid: false,
413
+ error: "Avatar must be a valid HTTP, IPFS, Arweave, or NFT URI"
414
+ };
415
+ }
416
+ return { valid: true };
417
+ }
418
+ async function validateAvatarFull(uri, options) {
419
+ const maxBytes = options?.maxBytes || MAX_AVATAR_BYTES;
420
+ const timeout = options?.timeout || 5e3;
421
+ const formatValidation = validateAvatarUri(uri);
422
+ if (!formatValidation.valid) {
423
+ throw new Error(formatValidation.error);
424
+ }
425
+ if (uri.startsWith("data:")) {
426
+ return { valid: true, sizeBytes: formatValidation.sizeBytes };
427
+ }
428
+ if (uri.startsWith("eip155:")) {
429
+ return { valid: true };
430
+ }
431
+ let checkUrl = uri;
432
+ if (uri.startsWith("ipfs://")) {
433
+ checkUrl = `https://ipfs.io/ipfs/${uri.slice(7)}`;
434
+ } else if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[0-9A-Za-z]{50,})/.test(uri)) {
435
+ checkUrl = `https://ipfs.io/ipfs/${uri}`;
436
+ } else if (uri.startsWith("ar://")) {
437
+ checkUrl = `https://arweave.net/${uri.slice(5)}`;
438
+ }
439
+ try {
440
+ const controller = new AbortController();
441
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
442
+ const response = await fetch(checkUrl, {
443
+ method: "HEAD",
444
+ signal: controller.signal
445
+ });
446
+ clearTimeout(timeoutId);
447
+ const contentLength = response.headers.get("content-length");
448
+ if (contentLength) {
449
+ const sizeBytes = parseInt(contentLength, 10);
450
+ if (sizeBytes > maxBytes) {
451
+ throw new Error(
452
+ `Avatar file size (${(sizeBytes / 1024).toFixed(1)}KB) exceeds ${maxBytes / 1024}KB limit. Please optimize as WebP or SVG for better performance.`
453
+ );
454
+ }
455
+ return { valid: true, sizeBytes };
456
+ }
457
+ console.warn(
458
+ `Could not determine avatar size for ${uri}. Ensure it's under ${maxBytes / 1024}KB.`
459
+ );
460
+ return { valid: true };
461
+ } catch (error) {
462
+ if (error.name === "AbortError") {
463
+ console.warn(`Avatar size check timed out for ${uri}`);
464
+ return { valid: true };
465
+ }
466
+ if (error.message.includes("exceeds")) {
467
+ throw error;
468
+ }
469
+ console.warn(`Could not check avatar size: ${error.message}`);
470
+ return { valid: true };
471
+ }
472
+ }
473
+ async function getRemoteAvatarSize(url) {
474
+ try {
475
+ const response = await fetch(url, { method: "HEAD" });
476
+ const contentLength = response.headers.get("content-length");
477
+ return contentLength ? parseInt(contentLength, 10) : null;
478
+ } catch {
479
+ return null;
480
+ }
481
+ }
482
+
483
+ // src/write.ts
347
484
  var controllerReadAbi = [
348
485
  {
349
486
  name: "available",
@@ -408,6 +545,10 @@ var resolverWriteAbi = [
408
545
  ];
409
546
  var ONE_YEAR = BigInt(365 * 24 * 60 * 60);
410
547
  async function getRegistrationInfo(label, userAddress, durationYears = 1, config) {
548
+ const validation = validateMonName(label);
549
+ if (!validation.valid) {
550
+ throw new Error(validation.error);
551
+ }
411
552
  const client = getMNSClient(config);
412
553
  const duration = ONE_YEAR * BigInt(durationYears);
413
554
  const [available, price, hasClaimed, balance] = await Promise.all([
@@ -435,6 +576,10 @@ async function getRegistrationInfo(label, userAddress, durationYears = 1, config
435
576
  return { available, price, isFreebie, hasClaimed, duration, label };
436
577
  }
437
578
  async function getRegisterTx(label, ownerAddress, durationYears = 1, config) {
579
+ const validation = validateMonName(label);
580
+ if (!validation.valid) {
581
+ throw new Error(validation.error);
582
+ }
438
583
  const info = await getRegistrationInfo(
439
584
  label,
440
585
  ownerAddress,
@@ -460,6 +605,17 @@ async function getRegisterTx(label, ownerAddress, durationYears = 1, config) {
460
605
  };
461
606
  }
462
607
  function getSetTextTx(name, key, value) {
608
+ if (key === "avatar" && value.startsWith("data:")) {
609
+ const validation = validateAvatarUri(value);
610
+ if (!validation.valid) {
611
+ throw new Error(validation.error);
612
+ }
613
+ }
614
+ if (key === "avatar" && !value.startsWith("data:")) {
615
+ console.warn(
616
+ "\u26A0\uFE0F Avatar set without size validation. For best practices, use setAvatarValidated() or validateAvatarFull() before calling setAvatar()."
617
+ );
618
+ }
463
619
  const node = (0, import_viem3.namehash)(
464
620
  name.endsWith(".mon") ? name : `${name}.mon`
465
621
  );
@@ -493,6 +649,8 @@ var TEXT_RECORD_KEYS = {
493
649
  };
494
650
  // Annotate the CommonJS export names for ESM import in node:
495
651
  0 && (module.exports = {
652
+ DEFAULT_AVATAR_PLACEHOLDER,
653
+ MAX_AVATAR_BYTES,
496
654
  MNS_BASE_REGISTRAR,
497
655
  MNS_CONTROLLER,
498
656
  MNS_PUBLIC_RESOLVER,
@@ -509,6 +667,7 @@ var TEXT_RECORD_KEYS = {
509
667
  getOwner,
510
668
  getRegisterTx,
511
669
  getRegistrationInfo,
670
+ getRemoteAvatarSize,
512
671
  getResolver,
513
672
  getSetAddrTx,
514
673
  getSetTextTx,
@@ -516,6 +675,9 @@ var TEXT_RECORD_KEYS = {
516
675
  lookupAddress,
517
676
  resolveInput,
518
677
  resolveName,
519
- resolverWriteAbi
678
+ resolverWriteAbi,
679
+ validateAvatarFull,
680
+ validateAvatarUri,
681
+ validateMonName
520
682
  });
521
683
  //# sourceMappingURL=index.cjs.map