@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/dist/react.d.cts CHANGED
@@ -204,6 +204,39 @@ declare function useMNSTextRecords(): {
204
204
  functionName: "setText";
205
205
  args: readonly [`0x${string}`, string, string];
206
206
  };
207
+ setAvatarValidated: (name: string, url: string) => Promise<{
208
+ address: "0xa2eb94c88e55d944aced2066c5cec9b759801f97";
209
+ abi: readonly [{
210
+ readonly name: "setText";
211
+ readonly type: "function";
212
+ readonly inputs: readonly [{
213
+ readonly name: "node";
214
+ readonly type: "bytes32";
215
+ }, {
216
+ readonly name: "key";
217
+ readonly type: "string";
218
+ }, {
219
+ readonly name: "value";
220
+ readonly type: "string";
221
+ }];
222
+ readonly outputs: readonly [];
223
+ readonly stateMutability: "nonpayable";
224
+ }, {
225
+ readonly name: "setAddr";
226
+ readonly type: "function";
227
+ readonly inputs: readonly [{
228
+ readonly name: "node";
229
+ readonly type: "bytes32";
230
+ }, {
231
+ readonly name: "a";
232
+ readonly type: "address";
233
+ }];
234
+ readonly outputs: readonly [];
235
+ readonly stateMutability: "nonpayable";
236
+ }];
237
+ functionName: "setText";
238
+ args: readonly [`0x${string}`, string, string];
239
+ }>;
207
240
  setUrl: (name: string, url: string) => {
208
241
  address: "0xa2eb94c88e55d944aced2066c5cec9b759801f97";
209
242
  abi: readonly [{
@@ -537,5 +570,59 @@ declare function useMNSAvatar(name: string | undefined, config?: MNSClientConfig
537
570
  loading: boolean;
538
571
  error: string | null;
539
572
  };
573
+ /**
574
+ * Owner hook: get the owner address of a .mon name
575
+ *
576
+ * @example
577
+ * ```tsx
578
+ * function OwnerInfo({ name }: { name: string }) {
579
+ * const { owner, loading } = useMNSOwner(name)
580
+ * if (loading) return <div>Loading...</div>
581
+ * return <div>Owner: {owner}</div>
582
+ * }
583
+ * ```
584
+ */
585
+ declare function useMNSOwner(name: string | undefined, config?: MNSClientConfig): {
586
+ owner: string | null;
587
+ loading: boolean;
588
+ error: string | null;
589
+ };
590
+ /**
591
+ * Resolver hook: get the resolver contract address for a .mon name
592
+ *
593
+ * @example
594
+ * ```tsx
595
+ * function ResolverInfo({ name }: { name: string }) {
596
+ * const { resolver, loading } = useMNSResolver(name)
597
+ * return <div>Resolver: {resolver}</div>
598
+ * }
599
+ * ```
600
+ */
601
+ declare function useMNSResolver(name: string | undefined, config?: MNSClientConfig): {
602
+ resolver: string | null;
603
+ loading: boolean;
604
+ error: string | null;
605
+ };
606
+ /**
607
+ * Expiry hook: get expiration info for a .mon name
608
+ *
609
+ * @example
610
+ * ```tsx
611
+ * function ExpiryInfo({ name }: { name: string }) {
612
+ * const { expiry, expiryDate, daysUntilExpiry, isExpired, loading } = useMNSExpiry(name)
613
+ * if (loading) return <div>Loading...</div>
614
+ * if (isExpired) return <div className="text-red-500">Expired!</div>
615
+ * return <div>Expires in {daysUntilExpiry} days</div>
616
+ * }
617
+ * ```
618
+ */
619
+ declare function useMNSExpiry(name: string | undefined, config?: MNSClientConfig): {
620
+ expiry: number | null;
621
+ expiryDate: Date | null;
622
+ daysUntilExpiry: number | null;
623
+ isExpired: boolean | null;
624
+ loading: boolean;
625
+ error: string | null;
626
+ };
540
627
 
541
- export { type MNSClientConfig, useMNSAddr, useMNSAddress, useMNSAvatar, useMNSDisplay, useMNSName, useMNSRegister, useMNSResolve, useMNSText, useMNSTextRecords, useRegistrationInfo };
628
+ export { type MNSClientConfig, useMNSAddr, useMNSAddress, useMNSAvatar, useMNSDisplay, useMNSExpiry, useMNSName, useMNSOwner, useMNSRegister, useMNSResolve, useMNSResolver, useMNSText, useMNSTextRecords, useRegistrationInfo };
package/dist/react.d.ts CHANGED
@@ -204,6 +204,39 @@ declare function useMNSTextRecords(): {
204
204
  functionName: "setText";
205
205
  args: readonly [`0x${string}`, string, string];
206
206
  };
207
+ setAvatarValidated: (name: string, url: string) => Promise<{
208
+ address: "0xa2eb94c88e55d944aced2066c5cec9b759801f97";
209
+ abi: readonly [{
210
+ readonly name: "setText";
211
+ readonly type: "function";
212
+ readonly inputs: readonly [{
213
+ readonly name: "node";
214
+ readonly type: "bytes32";
215
+ }, {
216
+ readonly name: "key";
217
+ readonly type: "string";
218
+ }, {
219
+ readonly name: "value";
220
+ readonly type: "string";
221
+ }];
222
+ readonly outputs: readonly [];
223
+ readonly stateMutability: "nonpayable";
224
+ }, {
225
+ readonly name: "setAddr";
226
+ readonly type: "function";
227
+ readonly inputs: readonly [{
228
+ readonly name: "node";
229
+ readonly type: "bytes32";
230
+ }, {
231
+ readonly name: "a";
232
+ readonly type: "address";
233
+ }];
234
+ readonly outputs: readonly [];
235
+ readonly stateMutability: "nonpayable";
236
+ }];
237
+ functionName: "setText";
238
+ args: readonly [`0x${string}`, string, string];
239
+ }>;
207
240
  setUrl: (name: string, url: string) => {
208
241
  address: "0xa2eb94c88e55d944aced2066c5cec9b759801f97";
209
242
  abi: readonly [{
@@ -537,5 +570,59 @@ declare function useMNSAvatar(name: string | undefined, config?: MNSClientConfig
537
570
  loading: boolean;
538
571
  error: string | null;
539
572
  };
573
+ /**
574
+ * Owner hook: get the owner address of a .mon name
575
+ *
576
+ * @example
577
+ * ```tsx
578
+ * function OwnerInfo({ name }: { name: string }) {
579
+ * const { owner, loading } = useMNSOwner(name)
580
+ * if (loading) return <div>Loading...</div>
581
+ * return <div>Owner: {owner}</div>
582
+ * }
583
+ * ```
584
+ */
585
+ declare function useMNSOwner(name: string | undefined, config?: MNSClientConfig): {
586
+ owner: string | null;
587
+ loading: boolean;
588
+ error: string | null;
589
+ };
590
+ /**
591
+ * Resolver hook: get the resolver contract address for a .mon name
592
+ *
593
+ * @example
594
+ * ```tsx
595
+ * function ResolverInfo({ name }: { name: string }) {
596
+ * const { resolver, loading } = useMNSResolver(name)
597
+ * return <div>Resolver: {resolver}</div>
598
+ * }
599
+ * ```
600
+ */
601
+ declare function useMNSResolver(name: string | undefined, config?: MNSClientConfig): {
602
+ resolver: string | null;
603
+ loading: boolean;
604
+ error: string | null;
605
+ };
606
+ /**
607
+ * Expiry hook: get expiration info for a .mon name
608
+ *
609
+ * @example
610
+ * ```tsx
611
+ * function ExpiryInfo({ name }: { name: string }) {
612
+ * const { expiry, expiryDate, daysUntilExpiry, isExpired, loading } = useMNSExpiry(name)
613
+ * if (loading) return <div>Loading...</div>
614
+ * if (isExpired) return <div className="text-red-500">Expired!</div>
615
+ * return <div>Expires in {daysUntilExpiry} days</div>
616
+ * }
617
+ * ```
618
+ */
619
+ declare function useMNSExpiry(name: string | undefined, config?: MNSClientConfig): {
620
+ expiry: number | null;
621
+ expiryDate: Date | null;
622
+ daysUntilExpiry: number | null;
623
+ isExpired: boolean | null;
624
+ loading: boolean;
625
+ error: string | null;
626
+ };
540
627
 
541
- export { type MNSClientConfig, useMNSAddr, useMNSAddress, useMNSAvatar, useMNSDisplay, useMNSName, useMNSRegister, useMNSResolve, useMNSText, useMNSTextRecords, useRegistrationInfo };
628
+ export { type MNSClientConfig, useMNSAddr, useMNSAddress, useMNSAvatar, useMNSDisplay, useMNSExpiry, useMNSName, useMNSOwner, useMNSRegister, useMNSResolve, useMNSResolver, useMNSText, useMNSTextRecords, useRegistrationInfo };
package/dist/react.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
6
6
 
7
7
  // src/resolve.ts
8
- import { namehash } from "viem";
8
+ import { namehash, keccak256, toBytes } from "viem";
9
9
 
10
10
  // src/client.ts
11
11
  import {
@@ -35,7 +35,9 @@ function getMNSClient(config) {
35
35
  var MNS_REGISTRY = "0x13f963486e741c8d3fcdc0a34a910920339a19ce";
36
36
  var MNS_PUBLIC_RESOLVER = "0xa2eb94c88e55d944aced2066c5cec9b759801f97";
37
37
  var MNS_CONTROLLER = "0x98866c55adbc73ec6c272bb3604ddbdee3f282a8";
38
+ var MNS_BASE_REGISTRAR = "0x104a49db9318c284d462841b6940bdb46624ca55";
38
39
  var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
40
+ var MAX_AVATAR_BYTES = 51200;
39
41
  var registryAbi = [
40
42
  {
41
43
  name: "resolver",
@@ -43,6 +45,13 @@ var registryAbi = [
43
45
  inputs: [{ name: "node", type: "bytes32" }],
44
46
  outputs: [{ name: "", type: "address" }],
45
47
  stateMutability: "view"
48
+ },
49
+ {
50
+ name: "owner",
51
+ type: "function",
52
+ inputs: [{ name: "node", type: "bytes32" }],
53
+ outputs: [{ name: "", type: "address" }],
54
+ stateMutability: "view"
46
55
  }
47
56
  ];
48
57
  var resolverAbi = [
@@ -167,21 +176,82 @@ async function getTextRecord(name, key, config) {
167
176
  return null;
168
177
  }
169
178
  }
170
- async function getAvatarUrl(name, config) {
171
- const raw = await getTextRecord(name, "avatar", config);
172
- if (!raw) return null;
173
- if (raw.startsWith("http://") || raw.startsWith("https://")) {
174
- return raw;
179
+ async function getOwner(name, config) {
180
+ try {
181
+ const client = getMNSClient(config);
182
+ const node = namehash(name.toLowerCase());
183
+ const owner = await client.readContract({
184
+ address: MNS_REGISTRY,
185
+ abi: registryAbi,
186
+ functionName: "owner",
187
+ args: [node]
188
+ });
189
+ return owner === ZERO_ADDRESS ? null : owner;
190
+ } catch {
191
+ return null;
175
192
  }
176
- if (raw.startsWith("ipfs://")) {
177
- const hash = raw.slice(7);
178
- return `https://ipfs.io/ipfs/${hash}`;
193
+ }
194
+ async function getResolver(name, config) {
195
+ try {
196
+ const client = getMNSClient(config);
197
+ const node = namehash(name.toLowerCase());
198
+ const resolver = await client.readContract({
199
+ address: MNS_REGISTRY,
200
+ abi: registryAbi,
201
+ functionName: "resolver",
202
+ args: [node]
203
+ });
204
+ return resolver === ZERO_ADDRESS ? null : resolver;
205
+ } catch {
206
+ return null;
179
207
  }
180
- const nftMatch = raw.match(
181
- /^eip155:(\d+)\/(erc721|erc1155):0x([a-fA-F0-9]{40})\/(\d+)$/
182
- );
183
- if (nftMatch) {
184
- try {
208
+ }
209
+ async function getExpiry(name, config) {
210
+ try {
211
+ const client = getMNSClient(config);
212
+ const label = name.replace(".mon", "");
213
+ const labelHash = keccak256(toBytes(label));
214
+ const expiry = await client.readContract({
215
+ address: MNS_BASE_REGISTRAR,
216
+ abi: [
217
+ {
218
+ name: "nameExpires",
219
+ type: "function",
220
+ inputs: [{ name: "id", type: "uint256" }],
221
+ outputs: [{ name: "", type: "uint256" }],
222
+ stateMutability: "view"
223
+ }
224
+ ],
225
+ functionName: "nameExpires",
226
+ args: [BigInt(labelHash)]
227
+ });
228
+ return Number(expiry);
229
+ } catch {
230
+ return null;
231
+ }
232
+ }
233
+ async function getAvatarUrl(name, config, options) {
234
+ const raw = await getTextRecord(name, "avatar", config);
235
+ if (!raw) return options?.fallback || null;
236
+ try {
237
+ if (raw.startsWith("http://") || raw.startsWith("https://")) {
238
+ return raw;
239
+ }
240
+ if (raw.startsWith("ipfs://")) {
241
+ const hash = raw.slice(7);
242
+ return `https://ipfs.io/ipfs/${hash}`;
243
+ }
244
+ if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[0-9A-Za-z]{50,})/.test(raw)) {
245
+ return `https://ipfs.io/ipfs/${raw}`;
246
+ }
247
+ if (raw.startsWith("ar://")) {
248
+ const hash = raw.slice(5);
249
+ return `https://arweave.net/${hash}`;
250
+ }
251
+ const nftMatch = raw.match(
252
+ /^eip155:(\d+)\/(erc721|erc1155):0x([a-fA-F0-9]{40})\/(\d+)$/
253
+ );
254
+ if (nftMatch) {
185
255
  const client = getMNSClient(config);
186
256
  const contract = `0x${nftMatch[3]}`;
187
257
  const tokenId = BigInt(nftMatch[4]);
@@ -209,15 +279,16 @@ async function getAvatarUrl(name, config) {
209
279
  if (image && image.startsWith("ipfs://")) {
210
280
  image = `https://ipfs.io/ipfs/${image.slice(7)}`;
211
281
  }
212
- return image;
213
- } catch {
214
- return null;
282
+ return image || options?.fallback || null;
215
283
  }
284
+ if (raw.startsWith("data:")) {
285
+ return raw;
286
+ }
287
+ return options?.fallback || null;
288
+ } catch (error) {
289
+ console.warn("Failed to resolve avatar:", error);
290
+ return options?.fallback || null;
216
291
  }
217
- if (raw.startsWith("data:")) {
218
- return raw;
219
- }
220
- return null;
221
292
  }
222
293
 
223
294
  // src/react-write.ts
@@ -225,6 +296,93 @@ import { useState, useEffect, useCallback } from "react";
225
296
 
226
297
  // src/write.ts
227
298
  import { namehash as namehash2 } from "viem";
299
+
300
+ // src/utils.ts
301
+ function validateAvatarUri(uri) {
302
+ if (!uri) {
303
+ return { valid: false, error: "Avatar URI is required" };
304
+ }
305
+ if (uri.startsWith("data:")) {
306
+ const sizeBytes = new Blob([uri]).size;
307
+ if (sizeBytes > MAX_AVATAR_BYTES) {
308
+ return {
309
+ valid: false,
310
+ error: `Avatar size (${(sizeBytes / 1024).toFixed(1)}KB) exceeds ${MAX_AVATAR_BYTES / 1024}KB limit. Please optimize as WebP or SVG for better performance.`,
311
+ sizeBytes
312
+ };
313
+ }
314
+ return { valid: true, sizeBytes };
315
+ }
316
+ const validSchemes = ["http://", "https://", "ipfs://", "eip155:", "ar://"];
317
+ const hasValidScheme = validSchemes.some((scheme) => uri.startsWith(scheme));
318
+ if (!hasValidScheme) {
319
+ if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[0-9A-Za-z]{50,})/.test(uri)) {
320
+ return { valid: true };
321
+ }
322
+ return {
323
+ valid: false,
324
+ error: "Avatar must be a valid HTTP, IPFS, Arweave, or NFT URI"
325
+ };
326
+ }
327
+ return { valid: true };
328
+ }
329
+ async function validateAvatarFull(uri, options) {
330
+ const maxBytes = options?.maxBytes || MAX_AVATAR_BYTES;
331
+ const timeout = options?.timeout || 5e3;
332
+ const formatValidation = validateAvatarUri(uri);
333
+ if (!formatValidation.valid) {
334
+ throw new Error(formatValidation.error);
335
+ }
336
+ if (uri.startsWith("data:")) {
337
+ return { valid: true, sizeBytes: formatValidation.sizeBytes };
338
+ }
339
+ if (uri.startsWith("eip155:")) {
340
+ return { valid: true };
341
+ }
342
+ let checkUrl = uri;
343
+ if (uri.startsWith("ipfs://")) {
344
+ checkUrl = `https://ipfs.io/ipfs/${uri.slice(7)}`;
345
+ } else if (/^(Qm[1-9A-HJ-NP-Za-km-z]{44}|bafy[0-9A-Za-z]{50,})/.test(uri)) {
346
+ checkUrl = `https://ipfs.io/ipfs/${uri}`;
347
+ } else if (uri.startsWith("ar://")) {
348
+ checkUrl = `https://arweave.net/${uri.slice(5)}`;
349
+ }
350
+ try {
351
+ const controller = new AbortController();
352
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
353
+ const response = await fetch(checkUrl, {
354
+ method: "HEAD",
355
+ signal: controller.signal
356
+ });
357
+ clearTimeout(timeoutId);
358
+ const contentLength = response.headers.get("content-length");
359
+ if (contentLength) {
360
+ const sizeBytes = parseInt(contentLength, 10);
361
+ if (sizeBytes > maxBytes) {
362
+ throw new Error(
363
+ `Avatar file size (${(sizeBytes / 1024).toFixed(1)}KB) exceeds ${maxBytes / 1024}KB limit. Please optimize as WebP or SVG for better performance.`
364
+ );
365
+ }
366
+ return { valid: true, sizeBytes };
367
+ }
368
+ console.warn(
369
+ `Could not determine avatar size for ${uri}. Ensure it's under ${maxBytes / 1024}KB.`
370
+ );
371
+ return { valid: true };
372
+ } catch (error) {
373
+ if (error.name === "AbortError") {
374
+ console.warn(`Avatar size check timed out for ${uri}`);
375
+ return { valid: true };
376
+ }
377
+ if (error.message.includes("exceeds")) {
378
+ throw error;
379
+ }
380
+ console.warn(`Could not check avatar size: ${error.message}`);
381
+ return { valid: true };
382
+ }
383
+ }
384
+
385
+ // src/write.ts
228
386
  var controllerReadAbi = [
229
387
  {
230
388
  name: "available",
@@ -341,6 +499,17 @@ async function getRegisterTx(label, ownerAddress, durationYears = 1, config) {
341
499
  };
342
500
  }
343
501
  function getSetTextTx(name, key, value) {
502
+ if (key === "avatar" && value.startsWith("data:")) {
503
+ const validation = validateAvatarUri(value);
504
+ if (!validation.valid) {
505
+ throw new Error(validation.error);
506
+ }
507
+ }
508
+ if (key === "avatar" && !value.startsWith("data:")) {
509
+ console.warn(
510
+ "\u26A0\uFE0F Avatar set without size validation. For best practices, use setAvatarValidated() or validateAvatarFull() before calling setAvatar()."
511
+ );
512
+ }
344
513
  const node = namehash2(
345
514
  name.endsWith(".mon") ? name : `${name}.mon`
346
515
  );
@@ -444,6 +613,10 @@ function useMNSTextRecords() {
444
613
  const setAvatar = useCallback((name, url) => {
445
614
  return getSetTextTx(name, "avatar", url);
446
615
  }, []);
616
+ const setAvatarValidated = useCallback(async (name, url) => {
617
+ await validateAvatarFull(url);
618
+ return getSetTextTx(name, "avatar", url);
619
+ }, []);
447
620
  const setUrl = useCallback((name, url) => {
448
621
  return getSetTextTx(name, "url", url);
449
622
  }, []);
@@ -462,6 +635,7 @@ function useMNSTextRecords() {
462
635
  return {
463
636
  setTextRecord,
464
637
  setAvatar,
638
+ setAvatarValidated,
465
639
  setUrl,
466
640
  setTwitter,
467
641
  setGithub,
@@ -626,14 +800,95 @@ function useMNSAvatar(name, config) {
626
800
  }, [name]);
627
801
  return { url, loading, error };
628
802
  }
803
+ function useMNSOwner(name, config) {
804
+ const [owner, setOwner] = useState2(null);
805
+ const [loading, setLoading] = useState2(false);
806
+ const [error, setError] = useState2(null);
807
+ useEffect2(() => {
808
+ if (!name) {
809
+ setOwner(null);
810
+ return;
811
+ }
812
+ let cancelled = false;
813
+ setLoading(true);
814
+ setError(null);
815
+ getOwner(name, config).then((o) => {
816
+ if (!cancelled) setOwner(o);
817
+ }).catch((e) => {
818
+ if (!cancelled) setError(e.message);
819
+ }).finally(() => {
820
+ if (!cancelled) setLoading(false);
821
+ });
822
+ return () => {
823
+ cancelled = true;
824
+ };
825
+ }, [name]);
826
+ return { owner, loading, error };
827
+ }
828
+ function useMNSResolver(name, config) {
829
+ const [resolver, setResolver] = useState2(null);
830
+ const [loading, setLoading] = useState2(false);
831
+ const [error, setError] = useState2(null);
832
+ useEffect2(() => {
833
+ if (!name) {
834
+ setResolver(null);
835
+ return;
836
+ }
837
+ let cancelled = false;
838
+ setLoading(true);
839
+ setError(null);
840
+ getResolver(name, config).then((r) => {
841
+ if (!cancelled) setResolver(r);
842
+ }).catch((e) => {
843
+ if (!cancelled) setError(e.message);
844
+ }).finally(() => {
845
+ if (!cancelled) setLoading(false);
846
+ });
847
+ return () => {
848
+ cancelled = true;
849
+ };
850
+ }, [name]);
851
+ return { resolver, loading, error };
852
+ }
853
+ function useMNSExpiry(name, config) {
854
+ const [expiry, setExpiry] = useState2(null);
855
+ const [loading, setLoading] = useState2(false);
856
+ const [error, setError] = useState2(null);
857
+ useEffect2(() => {
858
+ if (!name) {
859
+ setExpiry(null);
860
+ return;
861
+ }
862
+ let cancelled = false;
863
+ setLoading(true);
864
+ setError(null);
865
+ getExpiry(name, config).then((e) => {
866
+ if (!cancelled) setExpiry(e);
867
+ }).catch((e) => {
868
+ if (!cancelled) setError(e.message);
869
+ }).finally(() => {
870
+ if (!cancelled) setLoading(false);
871
+ });
872
+ return () => {
873
+ cancelled = true;
874
+ };
875
+ }, [name]);
876
+ const expiryDate = expiry ? new Date(expiry * 1e3) : null;
877
+ const daysUntilExpiry = expiry ? Math.floor((expiry * 1e3 - Date.now()) / (1e3 * 60 * 60 * 24)) : null;
878
+ const isExpired = expiry ? expiry * 1e3 < Date.now() : null;
879
+ return { expiry, expiryDate, daysUntilExpiry, isExpired, loading, error };
880
+ }
629
881
  export {
630
882
  useMNSAddr,
631
883
  useMNSAddress,
632
884
  useMNSAvatar,
633
885
  useMNSDisplay,
886
+ useMNSExpiry,
634
887
  useMNSName,
888
+ useMNSOwner,
635
889
  useMNSRegister,
636
890
  useMNSResolve,
891
+ useMNSResolver,
637
892
  useMNSText,
638
893
  useMNSTextRecords,
639
894
  useRegistrationInfo