@monadns/sdk 2.0.0 → 2.2.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
 
@@ -15,10 +15,10 @@ import { resolveName, lookupAddress } from '@monadns/sdk';
15
15
 
16
16
  // Forward: name → address
17
17
  const address = await resolveName('alice.mon');
18
- // '0xa05a8BF1eda5bbC2b3aCAF03D04f77bD7d66Cc47'
18
+ // '0xa1238BF1eda5bbC2b3aCAF03D04f77bD7d66Cc47'
19
19
 
20
20
  // Reverse: address → name
21
- const name = await lookupAddress('0xa05a8BF1eda5bbC2b3aCAF03D04f77bD7d66Cc47');
21
+ const name = await lookupAddress('0xa1238BF1eda5bbC2b3aCAF03D04f77bD7d66Cc47');
22
22
  // 'alice.mon'
23
23
  ```
24
24
 
@@ -92,6 +92,34 @@ function Twitter({ name }: { name: string }) {
92
92
  }
93
93
  ```
94
94
 
95
+ ### Check ownership and expiry
96
+
97
+ ```tsx
98
+ import { useMNSOwner, useMNSExpiry } from '@monadns/sdk/react';
99
+
100
+ function NameInfo({ name }: { name: string }) {
101
+ const { owner, loading: ownerLoading } = useMNSOwner(name);
102
+ const {
103
+ daysUntilExpiry,
104
+ isExpired,
105
+ loading: expiryLoading,
106
+ } = useMNSExpiry(name);
107
+
108
+ if (ownerLoading || expiryLoading) return <div>Loading...</div>;
109
+
110
+ return (
111
+ <div>
112
+ <p>Owner: {owner}</p>
113
+ {isExpired ? (
114
+ <p className="text-red-500">This name has expired!</p>
115
+ ) : (
116
+ <p>Expires in {daysUntilExpiry} days</p>
117
+ )}
118
+ </div>
119
+ );
120
+ }
121
+ ```
122
+
95
123
  ## Registration (with wagmi)
96
124
 
97
125
  The SDK returns transaction parameters that work directly with wagmi's `useWriteContract`:
@@ -275,6 +303,163 @@ function ProfileEditor({ name }: { name: string }) {
275
303
  }
276
304
  ```
277
305
 
306
+ ## Avatar Best Practices
307
+
308
+ ### Recommended Workflow (Enforced by SDK)
309
+
310
+ ```tsx
311
+ import {
312
+ validateAvatarFull,
313
+ useMNSTextRecords,
314
+ MAX_AVATAR_BYTES,
315
+ } from '@monadns/sdk/react';
316
+ import { useWriteContract } from 'wagmi';
317
+
318
+ function AvatarUploader({ name }: { name: string }) {
319
+ const [error, setError] = useState('');
320
+ const { setAvatarValidated } = useMNSTextRecords();
321
+ const { writeContract } = useWriteContract();
322
+
323
+ const handleUpload = async (url: string) => {
324
+ try {
325
+ setError('');
326
+
327
+ // SDK validates size automatically (throws if > 50KB)
328
+ const tx = await setAvatarValidated(name, url);
329
+ writeContract(tx);
330
+ } catch (e: any) {
331
+ setError(e.message);
332
+ // "Avatar file size (125.3KB) exceeds 50KB limit. Please optimize..."
333
+ }
334
+ };
335
+
336
+ return (
337
+ <div>
338
+ <input onChange={(e) => handleUpload(e.target.value)} />
339
+ {error && <p className="text-red-500">{error}</p>}
340
+ <p className="text-sm text-gray-500">
341
+ Max size: {(MAX_AVATAR_BYTES / 1024).toFixed(0)}KB (WebP/SVG
342
+ recommended)
343
+ </p>
344
+ </div>
345
+ );
346
+ }
347
+ ```
348
+
349
+ ### Size Limits (Automatically Enforced)
350
+
351
+ - **Data URIs**: Strict 50KB limit (enforced immediately)
352
+ - **Remote URLs** (IPFS/HTTP): Validated when using `setAvatarValidated()`
353
+ - **NFT avatars**: No size check (resolved at display time)
354
+
355
+ ### Supported Avatar Formats
356
+
357
+ ```typescript
358
+ // ✅ HTTPS URLs
359
+ setAvatar('alice.mon', 'https://example.com/avatar.png');
360
+
361
+ // ✅ IPFS with protocol
362
+ setAvatar('alice.mon', 'ipfs://QmX...');
363
+
364
+ // ✅ Raw IPFS CID (auto-converted)
365
+ setAvatar('alice.mon', 'QmX...'); // → ipfs://QmX...
366
+
367
+ // ✅ Arweave
368
+ setAvatar('alice.mon', 'ar://abc123...');
369
+
370
+ // ✅ NFT avatar
371
+ setAvatar('alice.mon', 'eip155:1/erc721:0x.../123');
372
+
373
+ // ✅ Data URI (with size check)
374
+ setAvatar('alice.mon', 'data:image/svg+xml;base64,...');
375
+ ```
376
+
377
+ ### Manual Validation (Advanced)
378
+
379
+ ```typescript
380
+ import { validateAvatarUri, validateAvatarFull } from '@monadns/sdk';
381
+
382
+ // Quick format check (synchronous)
383
+ const validation = validateAvatarUri(avatarUrl);
384
+ if (!validation.valid) {
385
+ console.error(validation.error);
386
+ }
387
+
388
+ // Full validation including size check (async)
389
+ try {
390
+ await validateAvatarFull(avatarUrl);
391
+ // Safe to use
392
+ } catch (error) {
393
+ console.error(error.message);
394
+ // "Avatar file size (65.3KB) exceeds 50KB limit..."
395
+ }
396
+
397
+ // Check remote file size
398
+ import { getRemoteAvatarSize, MAX_AVATAR_BYTES } from '@monadns/sdk';
399
+
400
+ const size = await getRemoteAvatarSize('https://example.com/avatar.png');
401
+ if (size && size > MAX_AVATAR_BYTES) {
402
+ alert(`Avatar too large: ${(size / 1024).toFixed(1)}KB. Max is 50KB.`);
403
+ }
404
+ ```
405
+
406
+ ### Fallback Avatars
407
+
408
+ ```typescript
409
+ import { getAvatarUrl, DEFAULT_AVATAR_PLACEHOLDER } from '@monadns/sdk';
410
+
411
+ // Use fallback if avatar not set or fails to resolve
412
+ const avatar = await getAvatarUrl('alice.mon', undefined, {
413
+ fallback: DEFAULT_AVATAR_PLACEHOLDER,
414
+ });
415
+ ```
416
+
417
+ ### Override Validation (Not Recommended)
418
+
419
+ If you need to bypass validation (not recommended for production):
420
+
421
+ ```typescript
422
+ // Use basic setAvatar() - only validates data URIs
423
+ const tx = setAvatar(name, largeAvatarUrl); // Logs warning
424
+ ```
425
+
426
+ ## Advanced Usage
427
+
428
+ ### Check Name Ownership & Expiry
429
+
430
+ ```typescript
431
+ import { getOwner, getExpiry, getResolver } from '@monadns/sdk';
432
+
433
+ // Check who owns a name
434
+ const owner = await getOwner('alice.mon');
435
+ console.log('Owner:', owner);
436
+
437
+ // Check when it expires
438
+ const expiry = await getExpiry('alice.mon');
439
+ const expiryDate = new Date(expiry * 1000);
440
+ const daysLeft = Math.floor(
441
+ (expiry * 1000 - Date.now()) / (1000 * 60 * 60 * 24),
442
+ );
443
+ console.log(`Expires: ${expiryDate.toLocaleDateString()} (${daysLeft} days)`);
444
+
445
+ // Get resolver contract
446
+ const resolver = await getResolver('alice.mon');
447
+ console.log('Resolver:', resolver);
448
+ ```
449
+
450
+ ### Check Availability Before Registration
451
+
452
+ ```typescript
453
+ import { getAvailable } from '@monadns/sdk';
454
+
455
+ const available = await getAvailable('alice.mon');
456
+ if (available) {
457
+ console.log('alice.mon is available for registration!');
458
+ } else {
459
+ console.log('alice.mon is already taken');
460
+ }
461
+ ```
462
+
278
463
  ## API Reference
279
464
 
280
465
  ### Core Functions (`@monadns/sdk`)
@@ -286,28 +471,48 @@ function ProfileEditor({ name }: { name: string }) {
286
471
  | `getDisplayName(address)` | Returns `.mon` name or truncated address |
287
472
  | `resolveInput(input)` | Accepts name or address, returns address |
288
473
  | `getTextRecord(name, key)` | Get text record (avatar, url, etc.) |
289
- | `getAvatarUrl(name)` | Get resolved avatar URL (handles IPFS, NFTs) |
290
- | `isRegistered(name)` | Check if a name is taken |
474
+ | `getAvatarUrl(name, config, options)` | Get resolved avatar URL (handles IPFS, NFTs) |
475
+ | `getAvailable(name)` | Check if a name is available for registration |
476
+ | `getOwner(name)` | Get the owner address of a name |
477
+ | `getResolver(name)` | Get the resolver contract address |
478
+ | `getExpiry(name)` | Get expiry timestamp (Unix seconds) |
291
479
  | `clearCache()` | Clear the resolution cache |
292
480
  | `getRegistrationInfo(label, address)` | Get price, availability, freebie status |
293
481
  | `getRegisterTx(label, address)` | Get tx params for registration |
294
482
  | `getSetTextTx(name, key, value)` | Get tx params for setting a text record |
295
483
  | `getSetAddrTx(name, address)` | Get tx params for updating the address record |
484
+ | `validateAvatarUri(uri)` | Validate avatar format and data URI size |
485
+ | `validateAvatarFull(uri)` | Async validation including remote file size |
486
+ | `getRemoteAvatarSize(url)` | Get file size of remote URL (helper for UI) |
296
487
 
297
488
  ### React Hooks (`@monadns/sdk/react`)
298
489
 
299
- | Hook | Returns | Description |
300
- | ------------------------------------- | -------------------------------------------- | ----------------------------- |
301
- | `useMNSName(address)` | `{ name, loading, error }` | Reverse resolution |
302
- | `useMNSAddress(name)` | `{ address, loading, error }` | Forward resolution |
303
- | `useMNSDisplay(address)` | `{ displayName, monName, loading }` | Display-ready name |
304
- | `useMNSResolve()` | `{ resolve, address, name, loading, error }` | On-demand resolver for inputs |
305
- | `useMNSText(name, key)` | `{ value, loading, error }` | Read text records |
306
- | `useMNSAvatar(name)` | `{ url, loading, error }` | Resolved avatar URL |
307
- | `useRegistrationInfo(label, address)` | `{ info, loading, error }` | Pre-registration data |
308
- | `useMNSRegister()` | `{ prepare, tx, loading, error }` | Prepare registration tx |
309
- | `useMNSTextRecords()` | `{ setAvatar, setTwitter, ... }` | Prepare text record txs |
310
- | `useMNSAddr()` | `{ setAddr }` | Prepare address update tx |
490
+ | Hook | Returns | Description |
491
+ | ------------------------------------- | -------------------------------------------------------------------- | ----------------------------- |
492
+ | `useMNSName(address)` | `{ name, loading, error }` | Reverse resolution |
493
+ | `useMNSAddress(name)` | `{ address, loading, error }` | Forward resolution |
494
+ | `useMNSDisplay(address)` | `{ displayName, monName, loading }` | Display-ready name |
495
+ | `useMNSResolve()` | `{ resolve, address, name, loading, error }` | On-demand resolver for inputs |
496
+ | `useMNSText(name, key)` | `{ value, loading, error }` | Read text records |
497
+ | `useMNSAvatar(name)` | `{ url, loading, error }` | Resolved avatar URL |
498
+ | `useMNSOwner(name)` | `{ owner, loading, error }` | Get name owner |
499
+ | `useMNSResolver(name)` | `{ resolver, loading, error }` | Get resolver address |
500
+ | `useMNSExpiry(name)` | `{ expiry, expiryDate, daysUntilExpiry, isExpired, loading, error }` | Get expiration info |
501
+ | `useRegistrationInfo(label, address)` | `{ info, loading, error }` | Pre-registration data |
502
+ | `useMNSRegister()` | `{ prepare, tx, loading, error }` | Prepare registration tx |
503
+ | `useMNSTextRecords()` | `{ setAvatar, setAvatarValidated, setTwitter, ... }` | Prepare text record txs |
504
+ | `useMNSAddr()` | `{ setAddr }` | Prepare address update tx |
505
+
506
+ ### Constants
507
+
508
+ | Constant | Value |
509
+ | ---------------------------- | -------------------------------------------- |
510
+ | `MAX_AVATAR_BYTES` | `51200` (50KB) |
511
+ | `DEFAULT_AVATAR_PLACEHOLDER` | Transparent 1x1 SVG data URI |
512
+ | `MNS_REGISTRY` | `0x13f963486e741c8d3fcdc0a34a910920339a19ce` |
513
+ | `MNS_PUBLIC_RESOLVER` | `0xa2eb94c88e55d944aced2066c5cec9b759801f97` |
514
+ | `MNS_CONTROLLER` | `0x98866c55adbc73ec6c272bb3604ddbdee3f282a8` |
515
+ | `MNS_BASE_REGISTRAR` | `0x104a49db9318c284d462841b6940bdb46624ca55` |
311
516
 
312
517
  ### Custom RPC
313
518
 
@@ -341,6 +546,49 @@ const name = await lookupAddress('0x...', { client });
341
546
 
342
547
  **Chain:** Monad Mainnet (Chain ID 143)
343
548
 
549
+ ## Migrating from v2.1 to v2.2
550
+
551
+ ### New Features
552
+
553
+ - **Avatar validation**: `validateAvatarUri()` and `validateAvatarFull()` for size checking
554
+ - **Avatar constants**: `MAX_AVATAR_BYTES` (50KB limit) and `DEFAULT_AVATAR_PLACEHOLDER`
555
+ - **Enhanced avatar resolution**: Support for raw IPFS CIDs and Arweave URIs
556
+ - **Validated avatar setter**: `setAvatarValidated()` hook with automatic size validation
557
+ - **Size helper**: `getRemoteAvatarSize()` for checking remote file sizes
558
+ - **Fallback support**: `getAvatarUrl()` now accepts fallback option
559
+
560
+ ### Behavior Changes
561
+
562
+ - Data URI avatars are now strictly limited to 50KB (throws error if exceeded)
563
+ - Setting non-data-URI avatars without validation logs a warning
564
+ - `getAvatarUrl()` now handles raw IPFS CIDs (e.g., `QmX...`) automatically
565
+
566
+ ## Migrating from v2.0 to v2.1
567
+
568
+ ### Breaking Changes
569
+
570
+ **`isRegistered()` has been renamed to `getAvailable()` with inverted logic**
571
+
572
+ **Before (v2.0):**
573
+
574
+ ```typescript
575
+ const registered = await isRegistered('alice.mon');
576
+ if (registered) {
577
+ console.log('Name is taken');
578
+ }
579
+ ```
580
+
581
+ **After (v2.1+):**
582
+
583
+ ```typescript
584
+ const available = await getAvailable('alice.mon');
585
+ if (available) {
586
+ console.log('Name is available for registration');
587
+ } else {
588
+ console.log('Name is taken');
589
+ }
590
+ ```
591
+
344
592
  ## Compatibility
345
593
 
346
594
  - **Frameworks:** React 17+, Next.js (App Router & Pages), Remix, Vite, CRA
@@ -350,4 +598,4 @@ const name = await lookupAddress('0x...', { client });
350
598
 
351
599
  ## License
352
600
 
353
- [Unlicense](https://unlicense.org) public domain, no attribution required.
601
+ [Unlicense](https://unlicense.org) public domain, no attribution required.
package/dist/index.cjs CHANGED
@@ -20,6 +20,9 @@ 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,
25
+ MNS_BASE_REGISTRAR: () => MNS_BASE_REGISTRAR,
23
26
  MNS_CONTROLLER: () => MNS_CONTROLLER,
24
27
  MNS_PUBLIC_RESOLVER: () => MNS_PUBLIC_RESOLVER,
25
28
  MNS_REGISTRY: () => MNS_REGISTRY,
@@ -27,19 +30,25 @@ __export(src_exports, {
27
30
  clearCache: () => clearCache,
28
31
  controllerReadAbi: () => controllerReadAbi,
29
32
  controllerWriteAbi: () => controllerWriteAbi,
33
+ getAvailable: () => getAvailable,
30
34
  getAvatarUrl: () => getAvatarUrl,
31
35
  getDisplayName: () => getDisplayName,
36
+ getExpiry: () => getExpiry,
32
37
  getMNSClient: () => getMNSClient,
38
+ getOwner: () => getOwner,
33
39
  getRegisterTx: () => getRegisterTx,
34
40
  getRegistrationInfo: () => getRegistrationInfo,
41
+ getRemoteAvatarSize: () => getRemoteAvatarSize,
42
+ getResolver: () => getResolver,
35
43
  getSetAddrTx: () => getSetAddrTx,
36
44
  getSetTextTx: () => getSetTextTx,
37
45
  getTextRecord: () => getTextRecord,
38
- isRegistered: () => isRegistered,
39
46
  lookupAddress: () => lookupAddress,
40
47
  resolveInput: () => resolveInput,
41
48
  resolveName: () => resolveName,
42
- resolverWriteAbi: () => resolverWriteAbi
49
+ resolverWriteAbi: () => resolverWriteAbi,
50
+ validateAvatarFull: () => validateAvatarFull,
51
+ validateAvatarUri: () => validateAvatarUri
43
52
  });
44
53
  module.exports = __toCommonJS(src_exports);
45
54
 
@@ -71,7 +80,10 @@ function getMNSClient(config) {
71
80
  var MNS_REGISTRY = "0x13f963486e741c8d3fcdc0a34a910920339a19ce";
72
81
  var MNS_PUBLIC_RESOLVER = "0xa2eb94c88e55d944aced2066c5cec9b759801f97";
73
82
  var MNS_CONTROLLER = "0x98866c55adbc73ec6c272bb3604ddbdee3f282a8";
83
+ var MNS_BASE_REGISTRAR = "0x104a49db9318c284d462841b6940bdb46624ca55";
74
84
  var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
85
+ var MAX_AVATAR_BYTES = 51200;
86
+ var DEFAULT_AVATAR_PLACEHOLDER = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"%3E%3C/svg%3E';
75
87
  var registryAbi = [
76
88
  {
77
89
  name: "resolver",
@@ -79,6 +91,13 @@ var registryAbi = [
79
91
  inputs: [{ name: "node", type: "bytes32" }],
80
92
  outputs: [{ name: "", type: "address" }],
81
93
  stateMutability: "view"
94
+ },
95
+ {
96
+ name: "owner",
97
+ type: "function",
98
+ inputs: [{ name: "node", type: "bytes32" }],
99
+ outputs: [{ name: "", type: "address" }],
100
+ stateMutability: "view"
82
101
  }
83
102
  ];
84
103
  var resolverAbi = [
@@ -203,6 +222,64 @@ async function getTextRecord(name, key, config) {
203
222
  return null;
204
223
  }
205
224
  }
225
+ async function getOwner(name, config) {
226
+ try {
227
+ const client = getMNSClient(config);
228
+ const node = (0, import_viem2.namehash)(name.toLowerCase());
229
+ const owner = await client.readContract({
230
+ address: MNS_REGISTRY,
231
+ abi: registryAbi,
232
+ functionName: "owner",
233
+ args: [node]
234
+ });
235
+ return owner === ZERO_ADDRESS ? null : owner;
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+ async function getResolver(name, config) {
241
+ try {
242
+ const client = getMNSClient(config);
243
+ const node = (0, import_viem2.namehash)(name.toLowerCase());
244
+ const resolver = await client.readContract({
245
+ address: MNS_REGISTRY,
246
+ abi: registryAbi,
247
+ functionName: "resolver",
248
+ args: [node]
249
+ });
250
+ return resolver === ZERO_ADDRESS ? null : resolver;
251
+ } catch {
252
+ return null;
253
+ }
254
+ }
255
+ async function getExpiry(name, config) {
256
+ try {
257
+ const client = getMNSClient(config);
258
+ const label = name.replace(".mon", "");
259
+ const labelHash = (0, import_viem2.keccak256)((0, import_viem2.toBytes)(label));
260
+ const expiry = await client.readContract({
261
+ address: MNS_BASE_REGISTRAR,
262
+ abi: [
263
+ {
264
+ name: "nameExpires",
265
+ type: "function",
266
+ inputs: [{ name: "id", type: "uint256" }],
267
+ outputs: [{ name: "", type: "uint256" }],
268
+ stateMutability: "view"
269
+ }
270
+ ],
271
+ functionName: "nameExpires",
272
+ args: [BigInt(labelHash)]
273
+ });
274
+ return Number(expiry);
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+ async function getAvailable(name, config) {
280
+ const addr = await resolveName(name, config);
281
+ return addr === null;
282
+ }
206
283
  async function getDisplayName(address, config) {
207
284
  const name = await lookupAddress(address, config);
208
285
  if (name) return name;
@@ -215,25 +292,28 @@ async function resolveInput(input, config) {
215
292
  if (trimmed.endsWith(".mon")) return resolveName(trimmed, config);
216
293
  return resolveName(`${trimmed}.mon`, config);
217
294
  }
218
- async function isRegistered(name, config) {
219
- const addr = await resolveName(name, config);
220
- return addr !== null;
221
- }
222
- async function getAvatarUrl(name, config) {
295
+ async function getAvatarUrl(name, config, options) {
223
296
  const raw = await getTextRecord(name, "avatar", config);
224
- if (!raw) return null;
225
- if (raw.startsWith("http://") || raw.startsWith("https://")) {
226
- return raw;
227
- }
228
- if (raw.startsWith("ipfs://")) {
229
- const hash = raw.slice(7);
230
- return `https://ipfs.io/ipfs/${hash}`;
231
- }
232
- const nftMatch = raw.match(
233
- /^eip155:(\d+)\/(erc721|erc1155):0x([a-fA-F0-9]{40})\/(\d+)$/
234
- );
235
- if (nftMatch) {
236
- try {
297
+ if (!raw) return options?.fallback || null;
298
+ try {
299
+ if (raw.startsWith("http://") || raw.startsWith("https://")) {
300
+ return raw;
301
+ }
302
+ if (raw.startsWith("ipfs://")) {
303
+ const hash = raw.slice(7);
304
+ return `https://ipfs.io/ipfs/${hash}`;
305
+ }
306
+ if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[0-9A-Za-z]{50,})/.test(raw)) {
307
+ return `https://ipfs.io/ipfs/${raw}`;
308
+ }
309
+ if (raw.startsWith("ar://")) {
310
+ const hash = raw.slice(5);
311
+ return `https://arweave.net/${hash}`;
312
+ }
313
+ const nftMatch = raw.match(
314
+ /^eip155:(\d+)\/(erc721|erc1155):0x([a-fA-F0-9]{40})\/(\d+)$/
315
+ );
316
+ if (nftMatch) {
237
317
  const client = getMNSClient(config);
238
318
  const contract = `0x${nftMatch[3]}`;
239
319
  const tokenId = BigInt(nftMatch[4]);
@@ -261,15 +341,16 @@ async function getAvatarUrl(name, config) {
261
341
  if (image && image.startsWith("ipfs://")) {
262
342
  image = `https://ipfs.io/ipfs/${image.slice(7)}`;
263
343
  }
264
- return image;
265
- } catch {
266
- return null;
344
+ return image || options?.fallback || null;
267
345
  }
346
+ if (raw.startsWith("data:")) {
347
+ return raw;
348
+ }
349
+ return options?.fallback || null;
350
+ } catch (error) {
351
+ console.warn("Failed to resolve avatar:", error);
352
+ return options?.fallback || null;
268
353
  }
269
- if (raw.startsWith("data:")) {
270
- return raw;
271
- }
272
- return null;
273
354
  }
274
355
  function clearCache() {
275
356
  nameCache.clear();
@@ -278,6 +359,102 @@ function clearCache() {
278
359
 
279
360
  // src/write.ts
280
361
  var import_viem3 = require("viem");
362
+
363
+ // src/utils.ts
364
+ function validateAvatarUri(uri) {
365
+ if (!uri) {
366
+ return { valid: false, error: "Avatar URI is required" };
367
+ }
368
+ if (uri.startsWith("data:")) {
369
+ const sizeBytes = new Blob([uri]).size;
370
+ if (sizeBytes > MAX_AVATAR_BYTES) {
371
+ return {
372
+ valid: false,
373
+ error: `Avatar size (${(sizeBytes / 1024).toFixed(1)}KB) exceeds ${MAX_AVATAR_BYTES / 1024}KB limit. Please optimize as WebP or SVG for better performance.`,
374
+ sizeBytes
375
+ };
376
+ }
377
+ return { valid: true, sizeBytes };
378
+ }
379
+ const validSchemes = ["http://", "https://", "ipfs://", "eip155:", "ar://"];
380
+ const hasValidScheme = validSchemes.some((scheme) => uri.startsWith(scheme));
381
+ if (!hasValidScheme) {
382
+ if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[0-9A-Za-z]{50,})/.test(uri)) {
383
+ return { valid: true };
384
+ }
385
+ return {
386
+ valid: false,
387
+ error: "Avatar must be a valid HTTP, IPFS, Arweave, or NFT URI"
388
+ };
389
+ }
390
+ return { valid: true };
391
+ }
392
+ async function validateAvatarFull(uri, options) {
393
+ const maxBytes = options?.maxBytes || MAX_AVATAR_BYTES;
394
+ const timeout = options?.timeout || 5e3;
395
+ const formatValidation = validateAvatarUri(uri);
396
+ if (!formatValidation.valid) {
397
+ throw new Error(formatValidation.error);
398
+ }
399
+ if (uri.startsWith("data:")) {
400
+ return { valid: true, sizeBytes: formatValidation.sizeBytes };
401
+ }
402
+ if (uri.startsWith("eip155:")) {
403
+ return { valid: true };
404
+ }
405
+ let checkUrl = uri;
406
+ if (uri.startsWith("ipfs://")) {
407
+ checkUrl = `https://ipfs.io/ipfs/${uri.slice(7)}`;
408
+ } else if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[0-9A-Za-z]{50,})/.test(uri)) {
409
+ checkUrl = `https://ipfs.io/ipfs/${uri}`;
410
+ } else if (uri.startsWith("ar://")) {
411
+ checkUrl = `https://arweave.net/${uri.slice(5)}`;
412
+ }
413
+ try {
414
+ const controller = new AbortController();
415
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
416
+ const response = await fetch(checkUrl, {
417
+ method: "HEAD",
418
+ signal: controller.signal
419
+ });
420
+ clearTimeout(timeoutId);
421
+ const contentLength = response.headers.get("content-length");
422
+ if (contentLength) {
423
+ const sizeBytes = parseInt(contentLength, 10);
424
+ if (sizeBytes > maxBytes) {
425
+ throw new Error(
426
+ `Avatar file size (${(sizeBytes / 1024).toFixed(1)}KB) exceeds ${maxBytes / 1024}KB limit. Please optimize as WebP or SVG for better performance.`
427
+ );
428
+ }
429
+ return { valid: true, sizeBytes };
430
+ }
431
+ console.warn(
432
+ `Could not determine avatar size for ${uri}. Ensure it's under ${maxBytes / 1024}KB.`
433
+ );
434
+ return { valid: true };
435
+ } catch (error) {
436
+ if (error.name === "AbortError") {
437
+ console.warn(`Avatar size check timed out for ${uri}`);
438
+ return { valid: true };
439
+ }
440
+ if (error.message.includes("exceeds")) {
441
+ throw error;
442
+ }
443
+ console.warn(`Could not check avatar size: ${error.message}`);
444
+ return { valid: true };
445
+ }
446
+ }
447
+ async function getRemoteAvatarSize(url) {
448
+ try {
449
+ const response = await fetch(url, { method: "HEAD" });
450
+ const contentLength = response.headers.get("content-length");
451
+ return contentLength ? parseInt(contentLength, 10) : null;
452
+ } catch {
453
+ return null;
454
+ }
455
+ }
456
+
457
+ // src/write.ts
281
458
  var controllerReadAbi = [
282
459
  {
283
460
  name: "available",
@@ -394,6 +571,17 @@ async function getRegisterTx(label, ownerAddress, durationYears = 1, config) {
394
571
  };
395
572
  }
396
573
  function getSetTextTx(name, key, value) {
574
+ if (key === "avatar" && value.startsWith("data:")) {
575
+ const validation = validateAvatarUri(value);
576
+ if (!validation.valid) {
577
+ throw new Error(validation.error);
578
+ }
579
+ }
580
+ if (key === "avatar" && !value.startsWith("data:")) {
581
+ console.warn(
582
+ "\u26A0\uFE0F Avatar set without size validation. For best practices, use setAvatarValidated() or validateAvatarFull() before calling setAvatar()."
583
+ );
584
+ }
397
585
  const node = (0, import_viem3.namehash)(
398
586
  name.endsWith(".mon") ? name : `${name}.mon`
399
587
  );
@@ -427,6 +615,9 @@ var TEXT_RECORD_KEYS = {
427
615
  };
428
616
  // Annotate the CommonJS export names for ESM import in node:
429
617
  0 && (module.exports = {
618
+ DEFAULT_AVATAR_PLACEHOLDER,
619
+ MAX_AVATAR_BYTES,
620
+ MNS_BASE_REGISTRAR,
430
621
  MNS_CONTROLLER,
431
622
  MNS_PUBLIC_RESOLVER,
432
623
  MNS_REGISTRY,
@@ -434,18 +625,24 @@ var TEXT_RECORD_KEYS = {
434
625
  clearCache,
435
626
  controllerReadAbi,
436
627
  controllerWriteAbi,
628
+ getAvailable,
437
629
  getAvatarUrl,
438
630
  getDisplayName,
631
+ getExpiry,
439
632
  getMNSClient,
633
+ getOwner,
440
634
  getRegisterTx,
441
635
  getRegistrationInfo,
636
+ getRemoteAvatarSize,
637
+ getResolver,
442
638
  getSetAddrTx,
443
639
  getSetTextTx,
444
640
  getTextRecord,
445
- isRegistered,
446
641
  lookupAddress,
447
642
  resolveInput,
448
643
  resolveName,
449
- resolverWriteAbi
644
+ resolverWriteAbi,
645
+ validateAvatarFull,
646
+ validateAvatarUri
450
647
  });
451
648
  //# sourceMappingURL=index.cjs.map