@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 +205 -25
- package/dist/index.cjs +185 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -3
- package/dist/index.d.ts +86 -3
- package/dist/index.js +178 -22
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +174 -25
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +33 -0
- package/dist/react.d.ts +33 -0
- package/dist/react.js +174 -25
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
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 {
|
|
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)`
|
|
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, ... }`
|
|
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
|
-
##
|
|
581
|
+
## Changelog
|
|
416
582
|
|
|
417
|
-
###
|
|
583
|
+
### v2.3.0
|
|
418
584
|
|
|
419
|
-
|
|
585
|
+
**New Features:**
|
|
420
586
|
|
|
421
|
-
**
|
|
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
|
-
|
|
424
|
-
const registered = await isRegistered('alice.mon');
|
|
425
|
-
if (registered) {
|
|
426
|
-
console.log('Name is taken');
|
|
427
|
-
}
|
|
428
|
-
```
|
|
592
|
+
**Improvements:**
|
|
429
593
|
|
|
430
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
/^
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|