@hybrd/xmtp 1.0.0 → 1.0.3
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/.cache/tsbuildinfo.json +1 -1
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-lint$colon$fix.log +6 -0
- package/dist/scripts/generate-keys.d.ts +1 -0
- package/dist/scripts/generate-keys.js +19 -0
- package/dist/scripts/refresh-identity.d.ts +1 -0
- package/dist/scripts/refresh-identity.js +93 -0
- package/dist/scripts/register-wallet.d.ts +1 -0
- package/dist/scripts/register-wallet.js +75 -0
- package/dist/scripts/revoke-all-installations.d.ts +2 -0
- package/dist/scripts/revoke-all-installations.js +68 -0
- package/dist/scripts/revoke-installations.d.ts +2 -0
- package/dist/scripts/revoke-installations.js +62 -0
- package/dist/src/abi/l2_resolver.d.ts +992 -0
- package/dist/src/abi/l2_resolver.js +699 -0
- package/dist/src/client.d.ts +76 -0
- package/dist/src/client.js +709 -0
- package/dist/src/constants.d.ts +3 -0
- package/dist/src/constants.js +6 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.js +46 -0
- package/dist/src/lib/message-listener.d.ts +69 -0
- package/dist/src/lib/message-listener.js +235 -0
- package/dist/src/lib/message-listener.test.d.ts +1 -0
- package/dist/src/lib/message-listener.test.js +303 -0
- package/dist/src/lib/subjects.d.ts +24 -0
- package/dist/src/lib/subjects.js +68 -0
- package/dist/src/resolver/address-resolver.d.ts +57 -0
- package/dist/src/resolver/address-resolver.js +168 -0
- package/dist/src/resolver/basename-resolver.d.ts +134 -0
- package/dist/src/resolver/basename-resolver.js +409 -0
- package/dist/src/resolver/ens-resolver.d.ts +95 -0
- package/dist/src/resolver/ens-resolver.js +249 -0
- package/dist/src/resolver/index.d.ts +1 -0
- package/dist/src/resolver/index.js +1 -0
- package/dist/src/resolver/resolver.d.ts +162 -0
- package/dist/src/resolver/resolver.js +238 -0
- package/dist/src/resolver/xmtp-resolver.d.ts +95 -0
- package/dist/src/resolver/xmtp-resolver.js +297 -0
- package/dist/src/service-client.d.ts +77 -0
- package/dist/src/service-client.js +198 -0
- package/dist/src/types.d.ts +123 -0
- package/dist/src/types.js +5 -0
- package/package.json +5 -4
- package/tsconfig.json +3 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type Address, PublicClient } from "viem";
|
|
2
|
+
export declare const BasenameTextRecordKeys: {
|
|
3
|
+
readonly Email: "email";
|
|
4
|
+
readonly Url: "url";
|
|
5
|
+
readonly Avatar: "avatar";
|
|
6
|
+
readonly Description: "description";
|
|
7
|
+
readonly Notice: "notice";
|
|
8
|
+
readonly Keywords: "keywords";
|
|
9
|
+
readonly Twitter: "com.twitter";
|
|
10
|
+
readonly Github: "com.github";
|
|
11
|
+
readonly Discord: "com.discord";
|
|
12
|
+
readonly Telegram: "org.telegram";
|
|
13
|
+
readonly Snapshot: "snapshot";
|
|
14
|
+
readonly Location: "location";
|
|
15
|
+
};
|
|
16
|
+
export type BasenameTextRecordKey = (typeof BasenameTextRecordKeys)[keyof typeof BasenameTextRecordKeys];
|
|
17
|
+
export type BaseName = string;
|
|
18
|
+
interface BasenameResolverOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Maximum number of basenames to cache
|
|
21
|
+
* @default 500
|
|
22
|
+
*/
|
|
23
|
+
maxCacheSize?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Cache TTL in milliseconds
|
|
26
|
+
* @default 3600000 (1 hour)
|
|
27
|
+
*/
|
|
28
|
+
cacheTtl?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Public client
|
|
31
|
+
* @default null
|
|
32
|
+
*/
|
|
33
|
+
publicClient: PublicClient;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Convert an chainId to a coinType hex for reverse chain resolution
|
|
37
|
+
*/
|
|
38
|
+
export declare const convertChainIdToCoinType: (chainId: number) => string;
|
|
39
|
+
/**
|
|
40
|
+
* Helper function to convert an address to its reverse node for ENS lookups
|
|
41
|
+
*/
|
|
42
|
+
export declare const convertReverseNodeToBytes: (address: Address, chainId: number) => `0x${string}`;
|
|
43
|
+
export declare class BasenameResolver {
|
|
44
|
+
private cache;
|
|
45
|
+
private textRecordCache;
|
|
46
|
+
private readonly maxCacheSize;
|
|
47
|
+
private readonly cacheTtl;
|
|
48
|
+
private readonly baseClient;
|
|
49
|
+
private resolverAddress;
|
|
50
|
+
private chainId;
|
|
51
|
+
constructor(options: BasenameResolverOptions);
|
|
52
|
+
/**
|
|
53
|
+
* Initialize the resolver address based on the client's chain ID
|
|
54
|
+
*/
|
|
55
|
+
private initializeResolver;
|
|
56
|
+
/**
|
|
57
|
+
* Get the resolver address, initializing if necessary
|
|
58
|
+
*/
|
|
59
|
+
private getResolverAddress;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a basename from an Ethereum address
|
|
62
|
+
*/
|
|
63
|
+
getBasename(address: Address): Promise<string | null>;
|
|
64
|
+
/**
|
|
65
|
+
* Get the avatar URL for a basename
|
|
66
|
+
*/
|
|
67
|
+
getBasenameAvatar(basename: BaseName): Promise<string | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Get a text record for a basename
|
|
70
|
+
*/
|
|
71
|
+
getBasenameTextRecord(basename: BaseName, key: BasenameTextRecordKey): Promise<string | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Get the Ethereum address that owns a basename
|
|
74
|
+
*/
|
|
75
|
+
getBasenameAddress(basename: BaseName): Promise<Address | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Get all basic metadata for a basename
|
|
78
|
+
*/
|
|
79
|
+
getBasenameMetadata(basename: BaseName): Promise<{
|
|
80
|
+
basename: string;
|
|
81
|
+
avatar: string | null;
|
|
82
|
+
description: string | null;
|
|
83
|
+
twitter: string | null;
|
|
84
|
+
github: string | null;
|
|
85
|
+
url: string | null;
|
|
86
|
+
} | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a full basename profile (name + metadata) from an address
|
|
89
|
+
*/
|
|
90
|
+
resolveBasenameProfile(address: Address): Promise<{
|
|
91
|
+
basename?: string | undefined;
|
|
92
|
+
avatar?: string | null | undefined;
|
|
93
|
+
description?: string | null | undefined;
|
|
94
|
+
twitter?: string | null | undefined;
|
|
95
|
+
github?: string | null | undefined;
|
|
96
|
+
url?: string | null | undefined;
|
|
97
|
+
address: `0x${string}`;
|
|
98
|
+
} | null>;
|
|
99
|
+
/**
|
|
100
|
+
* Get cached basename if not expired
|
|
101
|
+
*/
|
|
102
|
+
private getCachedBasename;
|
|
103
|
+
/**
|
|
104
|
+
* Cache basename with LRU eviction
|
|
105
|
+
*/
|
|
106
|
+
private setCachedBasename;
|
|
107
|
+
/**
|
|
108
|
+
* Get cached text record if not expired
|
|
109
|
+
*/
|
|
110
|
+
private getCachedTextRecord;
|
|
111
|
+
/**
|
|
112
|
+
* Cache text record
|
|
113
|
+
*/
|
|
114
|
+
private setCachedTextRecord;
|
|
115
|
+
/**
|
|
116
|
+
* Clear all caches
|
|
117
|
+
*/
|
|
118
|
+
clearCache(): void;
|
|
119
|
+
/**
|
|
120
|
+
* Get cache statistics
|
|
121
|
+
*/
|
|
122
|
+
getCacheStats(): {
|
|
123
|
+
basenameCache: {
|
|
124
|
+
size: number;
|
|
125
|
+
maxSize: number;
|
|
126
|
+
};
|
|
127
|
+
textRecordCache: {
|
|
128
|
+
size: number;
|
|
129
|
+
};
|
|
130
|
+
chainId: number | null;
|
|
131
|
+
resolverAddress: Address | null;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export {};
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { encodePacked, keccak256, namehash } from "viem";
|
|
2
|
+
import { mainnet } from "viem/chains";
|
|
3
|
+
import { L2ResolverAbi } from "../abi/l2_resolver";
|
|
4
|
+
// Base L2 Resolver Address mapping by chain ID
|
|
5
|
+
// const BASENAME_L2_RESOLVER_ADDRESSES: Record<number, Address> = {
|
|
6
|
+
// [mainnet.id]: "0x0000000000000000000000000000000000000000", // Mainnet (1)
|
|
7
|
+
// [base.id]: "0xC6d566A56A1aFf6508b41f6c90ff131615583BCD", // Base Mainnet (8453)
|
|
8
|
+
// [baseSepolia.id]: "0x6533C94869D28fAA8dF77cc63f9e2b2D6Cf77eBA" // Base Sepolia (84532)
|
|
9
|
+
// } as const
|
|
10
|
+
const BASENAME_L2_RESOLVER_ADDRESS = "0xC6d566A56A1aFf6508b41f6c90ff131615583BCD";
|
|
11
|
+
// Basename text record keys for metadata
|
|
12
|
+
export const BasenameTextRecordKeys = {
|
|
13
|
+
Email: "email",
|
|
14
|
+
Url: "url",
|
|
15
|
+
Avatar: "avatar",
|
|
16
|
+
Description: "description",
|
|
17
|
+
Notice: "notice",
|
|
18
|
+
Keywords: "keywords",
|
|
19
|
+
Twitter: "com.twitter",
|
|
20
|
+
Github: "com.github",
|
|
21
|
+
Discord: "com.discord",
|
|
22
|
+
Telegram: "org.telegram",
|
|
23
|
+
Snapshot: "snapshot",
|
|
24
|
+
Location: "location"
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Convert an chainId to a coinType hex for reverse chain resolution
|
|
28
|
+
*/
|
|
29
|
+
export const convertChainIdToCoinType = (chainId) => {
|
|
30
|
+
// L1 resolvers to addr
|
|
31
|
+
if (chainId === mainnet.id) {
|
|
32
|
+
return "addr";
|
|
33
|
+
}
|
|
34
|
+
const cointype = (0x80000000 | chainId) >>> 0;
|
|
35
|
+
return cointype.toString(16).toLocaleUpperCase();
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Helper function to convert an address to its reverse node for ENS lookups
|
|
39
|
+
*/
|
|
40
|
+
export const convertReverseNodeToBytes = (address, chainId) => {
|
|
41
|
+
const addressFormatted = address.toLocaleLowerCase();
|
|
42
|
+
const addressNode = keccak256(addressFormatted.substring(2));
|
|
43
|
+
const chainCoinType = convertChainIdToCoinType(chainId);
|
|
44
|
+
const baseReverseNode = namehash(`${chainCoinType.toLocaleUpperCase()}.reverse`);
|
|
45
|
+
const addressReverseNode = keccak256(encodePacked(["bytes32", "bytes32"], [baseReverseNode, addressNode]));
|
|
46
|
+
return addressReverseNode;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Helper function to convert a basename to its node hash
|
|
50
|
+
*/
|
|
51
|
+
function convertBasenameToNode(basename) {
|
|
52
|
+
return namehash(basename);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the resolver address for a given chain ID
|
|
56
|
+
*/
|
|
57
|
+
function getResolverAddress() {
|
|
58
|
+
const resolverAddress = BASENAME_L2_RESOLVER_ADDRESS;
|
|
59
|
+
return resolverAddress;
|
|
60
|
+
}
|
|
61
|
+
export class BasenameResolver {
|
|
62
|
+
constructor(options) {
|
|
63
|
+
this.cache = new Map();
|
|
64
|
+
this.textRecordCache = new Map();
|
|
65
|
+
this.resolverAddress = null;
|
|
66
|
+
this.chainId = null;
|
|
67
|
+
this.maxCacheSize = options.maxCacheSize ?? 500;
|
|
68
|
+
this.cacheTtl = options.cacheTtl ?? 3600000; // 1 hour
|
|
69
|
+
// Create a public client for Base network
|
|
70
|
+
this.baseClient = options.publicClient;
|
|
71
|
+
// Initialize resolver address lazily on first use
|
|
72
|
+
this.initializeResolver();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Initialize the resolver address based on the client's chain ID
|
|
76
|
+
*/
|
|
77
|
+
async initializeResolver() {
|
|
78
|
+
if (this.resolverAddress && this.chainId) {
|
|
79
|
+
console.log(`🔄 BasenameResolver already initialized for chain ${this.chainId} with resolver ${this.resolverAddress}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
console.log("🔄 Initializing BasenameResolver...");
|
|
84
|
+
this.chainId = await this.baseClient.getChainId();
|
|
85
|
+
console.log(`🔗 Chain ID detected: ${this.chainId}`);
|
|
86
|
+
this.resolverAddress = getResolverAddress();
|
|
87
|
+
console.log(`📍 Resolver address for chain ${this.chainId}: ${this.resolverAddress}`);
|
|
88
|
+
console.log(`✅ Initialized BasenameResolver for chain ${this.chainId} with resolver ${this.resolverAddress}`);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error("❌ Failed to initialize BasenameResolver:", error);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get the resolver address, initializing if necessary
|
|
97
|
+
*/
|
|
98
|
+
async getResolverAddress() {
|
|
99
|
+
await this.initializeResolver();
|
|
100
|
+
if (!this.resolverAddress) {
|
|
101
|
+
throw new Error("Failed to initialize resolver address");
|
|
102
|
+
}
|
|
103
|
+
return this.resolverAddress;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve a basename from an Ethereum address
|
|
107
|
+
*/
|
|
108
|
+
async getBasename(address) {
|
|
109
|
+
console.log(`🔍 Starting basename resolution for address: ${address}`);
|
|
110
|
+
try {
|
|
111
|
+
// Check cache first
|
|
112
|
+
const cached = this.getCachedBasename(address);
|
|
113
|
+
if (cached) {
|
|
114
|
+
console.log(`✅ Resolved basename from cache: ${cached}`);
|
|
115
|
+
return cached;
|
|
116
|
+
}
|
|
117
|
+
console.log(`📭 No cached basename found for address: ${address}`);
|
|
118
|
+
console.log("🔄 Getting resolver address...");
|
|
119
|
+
const resolverAddress = await this.getResolverAddress();
|
|
120
|
+
console.log(`📍 Using resolver address: ${resolverAddress}`);
|
|
121
|
+
console.log("🔄 Getting chain ID...");
|
|
122
|
+
const chainId = await this.baseClient.getChainId();
|
|
123
|
+
console.log(`🔗 Chain ID: ${chainId}`);
|
|
124
|
+
console.log("🔄 Converting address to reverse node...");
|
|
125
|
+
const addressReverseNode = convertReverseNodeToBytes(
|
|
126
|
+
// address.toUpperCase() as `0x${string}`,
|
|
127
|
+
address, chainId);
|
|
128
|
+
console.log(`🔗 Reverse node: ${addressReverseNode}`);
|
|
129
|
+
console.log("🔄 Reading contract to resolve basename...");
|
|
130
|
+
const basename = await this.baseClient.readContract({
|
|
131
|
+
abi: L2ResolverAbi,
|
|
132
|
+
address: resolverAddress,
|
|
133
|
+
functionName: "name",
|
|
134
|
+
args: [addressReverseNode]
|
|
135
|
+
});
|
|
136
|
+
console.log(`📋 Contract returned basename: "${basename}" (length: ${basename?.length || 0})`);
|
|
137
|
+
if (basename && basename.length > 0) {
|
|
138
|
+
this.setCachedBasename(address, basename);
|
|
139
|
+
console.log(`✅ Resolved basename: ${basename} for address: ${address}`);
|
|
140
|
+
return basename;
|
|
141
|
+
}
|
|
142
|
+
console.log(`❌ No basename found for address: ${address} (empty or null response)`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error(`❌ Error resolving basename for address ${address}:`, error);
|
|
147
|
+
if (error instanceof Error) {
|
|
148
|
+
console.error(`❌ Error details: ${error.message}`);
|
|
149
|
+
console.error(`❌ Error stack:`, error.stack);
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get the avatar URL for a basename
|
|
156
|
+
*/
|
|
157
|
+
async getBasenameAvatar(basename) {
|
|
158
|
+
console.log(`🖼️ Getting avatar for basename: ${basename}`);
|
|
159
|
+
return this.getBasenameTextRecord(basename, BasenameTextRecordKeys.Avatar);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get a text record for a basename
|
|
163
|
+
*/
|
|
164
|
+
async getBasenameTextRecord(basename, key) {
|
|
165
|
+
console.log(`📝 Getting text record "${key}" for basename: ${basename}`);
|
|
166
|
+
try {
|
|
167
|
+
// Check cache first
|
|
168
|
+
const cached = this.getCachedTextRecord(basename, key);
|
|
169
|
+
if (cached) {
|
|
170
|
+
console.log(`✅ Resolved text record from cache: ${key}=${cached}`);
|
|
171
|
+
return cached;
|
|
172
|
+
}
|
|
173
|
+
console.log(`📭 No cached text record found for ${basename}.${key}`);
|
|
174
|
+
console.log("🔄 Getting resolver address...");
|
|
175
|
+
const resolverAddress = await this.getResolverAddress();
|
|
176
|
+
console.log(`📍 Using resolver address: ${resolverAddress}`);
|
|
177
|
+
console.log("🔄 Converting basename to node...");
|
|
178
|
+
const node = convertBasenameToNode(basename);
|
|
179
|
+
console.log(`🔗 Node hash: ${node}`);
|
|
180
|
+
console.log(`🔄 Reading contract for text record "${key}"...`);
|
|
181
|
+
const textRecord = await this.baseClient.readContract({
|
|
182
|
+
abi: L2ResolverAbi,
|
|
183
|
+
address: resolverAddress,
|
|
184
|
+
functionName: "text",
|
|
185
|
+
args: [node, key]
|
|
186
|
+
});
|
|
187
|
+
console.log(`📋 Contract returned text record: "${textRecord}" (length: ${textRecord?.length || 0})`);
|
|
188
|
+
if (textRecord && textRecord.length > 0) {
|
|
189
|
+
this.setCachedTextRecord(basename, key, textRecord);
|
|
190
|
+
console.log(`✅ Resolved text record: ${key}=${textRecord}`);
|
|
191
|
+
return textRecord;
|
|
192
|
+
}
|
|
193
|
+
console.log(`❌ No text record found for ${basename}.${key} (empty or null response)`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
console.error(`❌ Error resolving text record ${key} for ${basename}:`, error);
|
|
198
|
+
if (error instanceof Error) {
|
|
199
|
+
console.error(`❌ Error details: ${error.message}`);
|
|
200
|
+
console.error(`❌ Error stack:`, error.stack);
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get the Ethereum address that owns a basename
|
|
207
|
+
*/
|
|
208
|
+
async getBasenameAddress(basename) {
|
|
209
|
+
console.log(`🔍 Getting address for basename: ${basename}`);
|
|
210
|
+
try {
|
|
211
|
+
console.log("🔄 Getting resolver address...");
|
|
212
|
+
const resolverAddress = await this.getResolverAddress();
|
|
213
|
+
console.log(`📍 Using resolver address: ${resolverAddress}`);
|
|
214
|
+
console.log("🔄 Converting basename to node...");
|
|
215
|
+
const node = convertBasenameToNode(basename);
|
|
216
|
+
console.log(`🔗 Node hash: ${node}`);
|
|
217
|
+
console.log("🔄 Reading contract to resolve address...");
|
|
218
|
+
const address = await this.baseClient.readContract({
|
|
219
|
+
abi: L2ResolverAbi,
|
|
220
|
+
address: resolverAddress,
|
|
221
|
+
functionName: "addr",
|
|
222
|
+
args: [node]
|
|
223
|
+
});
|
|
224
|
+
console.log(`📋 Contract returned address: "${address}"`);
|
|
225
|
+
if (address && address !== "0x0000000000000000000000000000000000000000") {
|
|
226
|
+
console.log(`✅ Resolved address: ${address} for basename: ${basename}`);
|
|
227
|
+
return address;
|
|
228
|
+
}
|
|
229
|
+
console.log(`❌ No address found for basename: ${basename} (zero address or null response)`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
console.error(`❌ Error resolving address for basename ${basename}:`, error);
|
|
234
|
+
if (error instanceof Error) {
|
|
235
|
+
console.error(`❌ Error details: ${error.message}`);
|
|
236
|
+
console.error(`❌ Error stack:`, error.stack);
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get all basic metadata for a basename
|
|
243
|
+
*/
|
|
244
|
+
async getBasenameMetadata(basename) {
|
|
245
|
+
console.log(`📊 Getting metadata for basename: ${basename}`);
|
|
246
|
+
try {
|
|
247
|
+
const [avatar, description, twitter, github, url] = await Promise.all([
|
|
248
|
+
this.getBasenameTextRecord(basename, BasenameTextRecordKeys.Avatar),
|
|
249
|
+
this.getBasenameTextRecord(basename, BasenameTextRecordKeys.Description),
|
|
250
|
+
this.getBasenameTextRecord(basename, BasenameTextRecordKeys.Twitter),
|
|
251
|
+
this.getBasenameTextRecord(basename, BasenameTextRecordKeys.Github),
|
|
252
|
+
this.getBasenameTextRecord(basename, BasenameTextRecordKeys.Url)
|
|
253
|
+
]);
|
|
254
|
+
const metadata = {
|
|
255
|
+
basename,
|
|
256
|
+
avatar,
|
|
257
|
+
description,
|
|
258
|
+
twitter,
|
|
259
|
+
github,
|
|
260
|
+
url
|
|
261
|
+
};
|
|
262
|
+
console.log(`✅ Resolved metadata for ${basename}:`, metadata);
|
|
263
|
+
return metadata;
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
console.error(`❌ Error resolving metadata for basename ${basename}:`, error);
|
|
267
|
+
if (error instanceof Error) {
|
|
268
|
+
console.error(`❌ Error details: ${error.message}`);
|
|
269
|
+
console.error(`❌ Error stack:`, error.stack);
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Resolve a full basename profile (name + metadata) from an address
|
|
276
|
+
*/
|
|
277
|
+
async resolveBasenameProfile(address) {
|
|
278
|
+
console.log(`👤 Resolving full basename profile for address: ${address}`);
|
|
279
|
+
try {
|
|
280
|
+
const basename = await this.getBasename(address);
|
|
281
|
+
if (!basename) {
|
|
282
|
+
console.log(`❌ No basename found for address: ${address}`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
console.log(`🔄 Getting metadata for resolved basename: ${basename}`);
|
|
286
|
+
const metadata = await this.getBasenameMetadata(basename);
|
|
287
|
+
const profile = {
|
|
288
|
+
address,
|
|
289
|
+
...metadata
|
|
290
|
+
};
|
|
291
|
+
console.log(`✅ Resolved full profile for ${address}:`, profile);
|
|
292
|
+
return profile;
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
console.error(`❌ Error resolving basename profile for ${address}:`, error);
|
|
296
|
+
if (error instanceof Error) {
|
|
297
|
+
console.error(`❌ Error details: ${error.message}`);
|
|
298
|
+
console.error(`❌ Error stack:`, error.stack);
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Get cached basename if not expired
|
|
305
|
+
*/
|
|
306
|
+
getCachedBasename(address) {
|
|
307
|
+
const entry = this.cache.get(address.toLowerCase());
|
|
308
|
+
if (!entry) {
|
|
309
|
+
console.log(`📭 No cache entry found for address: ${address.toLowerCase()}`);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const now = Date.now();
|
|
313
|
+
const age = now - entry.timestamp;
|
|
314
|
+
console.log(`🕐 Cache entry age: ${age}ms (TTL: ${this.cacheTtl}ms)`);
|
|
315
|
+
if (age > this.cacheTtl) {
|
|
316
|
+
console.log(`⏰ Cache entry expired for address: ${address.toLowerCase()}`);
|
|
317
|
+
this.cache.delete(address.toLowerCase());
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
console.log(`✅ Valid cache entry found for ${address.toLowerCase()}: "${entry.basename}"`);
|
|
321
|
+
return entry.basename;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Cache basename with LRU eviction
|
|
325
|
+
*/
|
|
326
|
+
setCachedBasename(address, basename) {
|
|
327
|
+
// Simple LRU: if cache is full, remove oldest entry
|
|
328
|
+
if (this.cache.size >= this.maxCacheSize) {
|
|
329
|
+
const firstKey = this.cache.keys().next().value;
|
|
330
|
+
if (firstKey) {
|
|
331
|
+
console.log(`🗑️ Cache full, removing oldest entry: ${firstKey}`);
|
|
332
|
+
this.cache.delete(firstKey);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
console.log(`💾 Caching basename "${basename}" for address: ${address.toLowerCase()}`);
|
|
336
|
+
this.cache.set(address.toLowerCase(), {
|
|
337
|
+
basename,
|
|
338
|
+
timestamp: Date.now()
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Get cached text record if not expired
|
|
343
|
+
*/
|
|
344
|
+
getCachedTextRecord(basename, key) {
|
|
345
|
+
const basenameCache = this.textRecordCache.get(basename);
|
|
346
|
+
if (!basenameCache) {
|
|
347
|
+
console.log(`📭 No text record cache found for basename: ${basename}`);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const entry = basenameCache.get(key);
|
|
351
|
+
if (!entry) {
|
|
352
|
+
console.log(`📭 No cached text record found for ${basename}.${key}`);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
const now = Date.now();
|
|
356
|
+
const age = now - entry.timestamp;
|
|
357
|
+
console.log(`🕐 Text record cache entry age: ${age}ms (TTL: ${this.cacheTtl}ms)`);
|
|
358
|
+
if (age > this.cacheTtl) {
|
|
359
|
+
console.log(`⏰ Text record cache entry expired for ${basename}.${key}`);
|
|
360
|
+
basenameCache.delete(key);
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
console.log(`✅ Valid text record cache entry found for ${basename}.${key}: "${entry.value}"`);
|
|
364
|
+
return entry.value;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Cache text record
|
|
368
|
+
*/
|
|
369
|
+
setCachedTextRecord(basename, key, value) {
|
|
370
|
+
let basenameCache = this.textRecordCache.get(basename);
|
|
371
|
+
if (!basenameCache) {
|
|
372
|
+
console.log(`📝 Creating new text record cache for basename: ${basename}`);
|
|
373
|
+
basenameCache = new Map();
|
|
374
|
+
this.textRecordCache.set(basename, basenameCache);
|
|
375
|
+
}
|
|
376
|
+
console.log(`💾 Caching text record "${key}" = "${value}" for basename: ${basename}`);
|
|
377
|
+
basenameCache.set(key, {
|
|
378
|
+
value,
|
|
379
|
+
timestamp: Date.now()
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Clear all caches
|
|
384
|
+
*/
|
|
385
|
+
clearCache() {
|
|
386
|
+
const basenameCount = this.cache.size;
|
|
387
|
+
const textRecordCount = this.textRecordCache.size;
|
|
388
|
+
this.cache.clear();
|
|
389
|
+
this.textRecordCache.clear();
|
|
390
|
+
console.log(`🗑️ Basename cache cleared (${basenameCount} entries removed)`);
|
|
391
|
+
console.log(`🗑️ Text record cache cleared (${textRecordCount} basename caches removed)`);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get cache statistics
|
|
395
|
+
*/
|
|
396
|
+
getCacheStats() {
|
|
397
|
+
return {
|
|
398
|
+
basenameCache: {
|
|
399
|
+
size: this.cache.size,
|
|
400
|
+
maxSize: this.maxCacheSize
|
|
401
|
+
},
|
|
402
|
+
textRecordCache: {
|
|
403
|
+
size: this.textRecordCache.size
|
|
404
|
+
},
|
|
405
|
+
chainId: this.chainId,
|
|
406
|
+
resolverAddress: this.resolverAddress
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type Address, PublicClient } from "viem";
|
|
2
|
+
interface ENSResolverOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Maximum number of ENS names to cache
|
|
5
|
+
* @default 500
|
|
6
|
+
*/
|
|
7
|
+
maxCacheSize?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Cache TTL in milliseconds
|
|
10
|
+
* @default 3600000 (1 hour)
|
|
11
|
+
*/
|
|
12
|
+
cacheTtl?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Mainnet public client for ENS resolution
|
|
15
|
+
*/
|
|
16
|
+
mainnetClient: PublicClient;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* ENS Resolver for mainnet .eth names
|
|
20
|
+
* Handles resolution of ENS names to addresses and reverse resolution
|
|
21
|
+
*/
|
|
22
|
+
export declare class ENSResolver {
|
|
23
|
+
private cache;
|
|
24
|
+
private reverseCache;
|
|
25
|
+
private readonly maxCacheSize;
|
|
26
|
+
private readonly cacheTtl;
|
|
27
|
+
private readonly mainnetClient;
|
|
28
|
+
constructor(options: ENSResolverOptions);
|
|
29
|
+
/**
|
|
30
|
+
* Resolve an ENS name to an Ethereum address
|
|
31
|
+
*/
|
|
32
|
+
resolveENSName(ensName: string): Promise<Address | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve an address to its primary ENS name (reverse resolution)
|
|
35
|
+
*/
|
|
36
|
+
resolveAddressToENS(address: Address): Promise<string | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Get ENS avatar for a name
|
|
39
|
+
*/
|
|
40
|
+
getENSAvatar(ensName: string): Promise<string | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Get ENS text record
|
|
43
|
+
*/
|
|
44
|
+
getENSTextRecord(ensName: string, key: string): Promise<string | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Get comprehensive ENS profile
|
|
47
|
+
*/
|
|
48
|
+
getENSProfile(ensName: string): Promise<{
|
|
49
|
+
ensName: string;
|
|
50
|
+
address: `0x${string}` | null;
|
|
51
|
+
avatar: string | null;
|
|
52
|
+
description: string | null;
|
|
53
|
+
twitter: string | null;
|
|
54
|
+
github: string | null;
|
|
55
|
+
url: string | null;
|
|
56
|
+
} | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Check if a name is a valid ENS name (.eth)
|
|
59
|
+
*/
|
|
60
|
+
isENSName(name: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Get cached address if not expired
|
|
63
|
+
*/
|
|
64
|
+
private getCachedAddress;
|
|
65
|
+
/**
|
|
66
|
+
* Cache address with LRU eviction
|
|
67
|
+
*/
|
|
68
|
+
private setCachedAddress;
|
|
69
|
+
/**
|
|
70
|
+
* Get cached ENS name if not expired
|
|
71
|
+
*/
|
|
72
|
+
private getCachedENSName;
|
|
73
|
+
/**
|
|
74
|
+
* Cache ENS name with LRU eviction
|
|
75
|
+
*/
|
|
76
|
+
private setCachedENSName;
|
|
77
|
+
/**
|
|
78
|
+
* Clear all caches
|
|
79
|
+
*/
|
|
80
|
+
clearCache(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Get cache statistics
|
|
83
|
+
*/
|
|
84
|
+
getCacheStats(): {
|
|
85
|
+
addressCache: {
|
|
86
|
+
size: number;
|
|
87
|
+
maxSize: number;
|
|
88
|
+
};
|
|
89
|
+
reverseCache: {
|
|
90
|
+
size: number;
|
|
91
|
+
maxSize: number;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export {};
|