@monadns/sdk 2.1.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
 
@@ -303,6 +303,126 @@ function ProfileEditor({ name }: { name: string }) {
303
303
  }
304
304
  ```
305
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
+
306
426
  ## Advanced Usage
307
427
 
308
428
  ### Check Name Ownership & Expiry
@@ -351,7 +471,7 @@ if (available) {
351
471
  | `getDisplayName(address)` | Returns `.mon` name or truncated address |
352
472
  | `resolveInput(input)` | Accepts name or address, returns address |
353
473
  | `getTextRecord(name, key)` | Get text record (avatar, url, etc.) |
354
- | `getAvatarUrl(name)` | Get resolved avatar URL (handles IPFS, NFTs) |
474
+ | `getAvatarUrl(name, config, options)` | Get resolved avatar URL (handles IPFS, NFTs) |
355
475
  | `getAvailable(name)` | Check if a name is available for registration |
356
476
  | `getOwner(name)` | Get the owner address of a name |
357
477
  | `getResolver(name)` | Get the resolver contract address |
@@ -361,6 +481,9 @@ if (available) {
361
481
  | `getRegisterTx(label, address)` | Get tx params for registration |
362
482
  | `getSetTextTx(name, key, value)` | Get tx params for setting a text record |
363
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) |
364
487
 
365
488
  ### React Hooks (`@monadns/sdk/react`)
366
489
 
@@ -377,9 +500,20 @@ if (available) {
377
500
  | `useMNSExpiry(name)` | `{ expiry, expiryDate, daysUntilExpiry, isExpired, loading, error }` | Get expiration info |
378
501
  | `useRegistrationInfo(label, address)` | `{ info, loading, error }` | Pre-registration data |
379
502
  | `useMNSRegister()` | `{ prepare, tx, loading, error }` | Prepare registration tx |
380
- | `useMNSTextRecords()` | `{ setAvatar, setTwitter, ... }` | Prepare text record txs |
503
+ | `useMNSTextRecords()` | `{ setAvatar, setAvatarValidated, setTwitter, ... }` | Prepare text record txs |
381
504
  | `useMNSAddr()` | `{ setAddr }` | Prepare address update tx |
382
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` |
516
+
383
517
  ### Custom RPC
384
518
 
385
519
  All functions accept an optional config object:
@@ -412,6 +546,23 @@ const name = await lookupAddress('0x...', { client });
412
546
 
413
547
  **Chain:** Monad Mainnet (Chain ID 143)
414
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
+
415
566
  ## Migrating from v2.0 to v2.1
416
567
 
417
568
  ### Breaking Changes
@@ -427,7 +578,7 @@ if (registered) {
427
578
  }
428
579
  ```
429
580
 
430
- **After (v2.1):**
581
+ **After (v2.1+):**
431
582
 
432
583
  ```typescript
433
584
  const available = await getAvailable('alice.mon');
@@ -438,16 +589,6 @@ if (available) {
438
589
  }
439
590
  ```
440
591
 
441
- ### New Features
442
-
443
- - `getOwner(name)` - Get the owner of a name
444
- - `getResolver(name)` - Get the resolver contract
445
- - `getExpiry(name)` - Get expiration timestamp
446
- - `useMNSOwner(name)` - React hook for ownership info
447
- - `useMNSResolver(name)` - React hook for resolver info
448
- - `useMNSExpiry(name)` - React hook for expiry info with computed fields
449
- - `MNS_BASE_REGISTRAR` constant now exported
450
-
451
592
  ## Compatibility
452
593
 
453
594
  - **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,9 @@ __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
47
52
  });
48
53
  module.exports = __toCommonJS(src_exports);
49
54
 
@@ -77,6 +82,8 @@ var MNS_PUBLIC_RESOLVER = "0xa2eb94c88e55d944aced2066c5cec9b759801f97";
77
82
  var MNS_CONTROLLER = "0x98866c55adbc73ec6c272bb3604ddbdee3f282a8";
78
83
  var MNS_BASE_REGISTRAR = "0x104a49db9318c284d462841b6940bdb46624ca55";
79
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';
80
87
  var registryAbi = [
81
88
  {
82
89
  name: "resolver",
@@ -285,21 +292,28 @@ async function resolveInput(input, config) {
285
292
  if (trimmed.endsWith(".mon")) return resolveName(trimmed, config);
286
293
  return resolveName(`${trimmed}.mon`, config);
287
294
  }
288
- async function getAvatarUrl(name, config) {
295
+ async function getAvatarUrl(name, config, options) {
289
296
  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 {
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) {
303
317
  const client = getMNSClient(config);
304
318
  const contract = `0x${nftMatch[3]}`;
305
319
  const tokenId = BigInt(nftMatch[4]);
@@ -327,15 +341,16 @@ async function getAvatarUrl(name, config) {
327
341
  if (image && image.startsWith("ipfs://")) {
328
342
  image = `https://ipfs.io/ipfs/${image.slice(7)}`;
329
343
  }
330
- return image;
331
- } catch {
332
- return null;
344
+ return image || options?.fallback || null;
333
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;
334
353
  }
335
- if (raw.startsWith("data:")) {
336
- return raw;
337
- }
338
- return null;
339
354
  }
340
355
  function clearCache() {
341
356
  nameCache.clear();
@@ -344,6 +359,102 @@ function clearCache() {
344
359
 
345
360
  // src/write.ts
346
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
347
458
  var controllerReadAbi = [
348
459
  {
349
460
  name: "available",
@@ -460,6 +571,17 @@ async function getRegisterTx(label, ownerAddress, durationYears = 1, config) {
460
571
  };
461
572
  }
462
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
+ }
463
585
  const node = (0, import_viem3.namehash)(
464
586
  name.endsWith(".mon") ? name : `${name}.mon`
465
587
  );
@@ -493,6 +615,8 @@ var TEXT_RECORD_KEYS = {
493
615
  };
494
616
  // Annotate the CommonJS export names for ESM import in node:
495
617
  0 && (module.exports = {
618
+ DEFAULT_AVATAR_PLACEHOLDER,
619
+ MAX_AVATAR_BYTES,
496
620
  MNS_BASE_REGISTRAR,
497
621
  MNS_CONTROLLER,
498
622
  MNS_PUBLIC_RESOLVER,
@@ -509,6 +633,7 @@ var TEXT_RECORD_KEYS = {
509
633
  getOwner,
510
634
  getRegisterTx,
511
635
  getRegistrationInfo,
636
+ getRemoteAvatarSize,
512
637
  getResolver,
513
638
  getSetAddrTx,
514
639
  getSetTextTx,
@@ -516,6 +641,8 @@ var TEXT_RECORD_KEYS = {
516
641
  lookupAddress,
517
642
  resolveInput,
518
643
  resolveName,
519
- resolverWriteAbi
644
+ resolverWriteAbi,
645
+ validateAvatarFull,
646
+ validateAvatarUri
520
647
  });
521
648
  //# sourceMappingURL=index.cjs.map