@1001-digital/layers.evm 1.0.4 → 1.0.6
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/app/composables/ens.ts +55 -35
- package/app/utils/cache.ts +59 -0
- package/app/utils/ens.ts +33 -24
- package/package.json +1 -1
package/app/composables/ens.ts
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
import { getPublicClient } from '@wagmi/core'
|
|
2
2
|
import type { Config } from '@wagmi/vue'
|
|
3
3
|
|
|
4
|
+
type EnsMode = 'indexer' | 'chain'
|
|
5
|
+
|
|
4
6
|
interface UseEnsOptions {
|
|
5
|
-
mode?: MaybeRefOrGetter<
|
|
7
|
+
mode?: MaybeRefOrGetter<EnsMode | undefined>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface EnsRuntimeConfig {
|
|
11
|
+
ens?: { indexer1?: string, indexer2?: string, indexer3?: string }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getIndexerUrls(config: EnsRuntimeConfig): string[] {
|
|
15
|
+
if (!config.ens) return []
|
|
16
|
+
return [config.ens.indexer1, config.ens.indexer2, config.ens.indexer3].filter(Boolean) as string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function resolve(
|
|
20
|
+
identifier: string,
|
|
21
|
+
strategies: EnsMode[],
|
|
22
|
+
indexerUrls: string[],
|
|
23
|
+
wagmi: Config,
|
|
24
|
+
chainKeys: string[],
|
|
25
|
+
): Promise<EnsProfile> {
|
|
26
|
+
for (const strategy of strategies) {
|
|
27
|
+
try {
|
|
28
|
+
if (strategy === 'indexer') {
|
|
29
|
+
if (!indexerUrls.length) continue
|
|
30
|
+
return await fetchEnsFromIndexer(identifier, indexerUrls)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (strategy === 'chain') {
|
|
34
|
+
const client = getPublicClient(wagmi, { chainId: 1 })
|
|
35
|
+
if (!client) continue
|
|
36
|
+
return await fetchEnsFromChain(identifier, client, chainKeys)
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { address: identifier, ens: null, data: null }
|
|
6
44
|
}
|
|
7
45
|
|
|
8
46
|
function useEnsBase(
|
|
9
|
-
|
|
47
|
+
tier: string,
|
|
10
48
|
identifier: MaybeRefOrGetter<string | undefined>,
|
|
11
49
|
chainKeys: string[],
|
|
12
50
|
options: UseEnsOptions = {},
|
|
@@ -15,53 +53,35 @@ function useEnsBase(
|
|
|
15
53
|
const appConfig = useAppConfig()
|
|
16
54
|
const runtimeConfig = useRuntimeConfig()
|
|
17
55
|
|
|
18
|
-
const mode = computed(() => toValue(options.mode) || appConfig.evm?.ens?.mode || 'indexer')
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const ens = (runtimeConfig.public.evm as { ens?: { indexer1?: string, indexer2?: string, indexer3?: string } }).ens
|
|
22
|
-
if (!ens) return []
|
|
23
|
-
return [ens.indexer1, ens.indexer2, ens.indexer3].filter(Boolean) as string[]
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
const strategies = computed(() => {
|
|
27
|
-
const primary = mode.value
|
|
28
|
-
const fallback = primary === 'indexer' ? 'chain' : 'indexer'
|
|
29
|
-
return [primary, fallback] as const
|
|
30
|
-
})
|
|
56
|
+
const mode = computed<EnsMode>(() => toValue(options.mode) || appConfig.evm?.ens?.mode || 'indexer')
|
|
57
|
+
const indexerUrls = computed(() => getIndexerUrls(runtimeConfig.public.evm as EnsRuntimeConfig))
|
|
58
|
+
const cacheKey = computed(() => `ens-${tier}-${toValue(identifier)}`)
|
|
31
59
|
|
|
32
60
|
return useAsyncData(
|
|
33
|
-
|
|
61
|
+
cacheKey.value,
|
|
34
62
|
async () => {
|
|
35
63
|
const id = toValue(identifier)
|
|
36
64
|
if (!id) return null
|
|
37
65
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!indexerUrls.value.length) continue
|
|
42
|
-
return await fetchEnsFromIndexer(id, indexerUrls.value)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (strategy === 'chain') {
|
|
46
|
-
const client = getPublicClient($wagmi as Config, { chainId: 1 })
|
|
47
|
-
if (!client) continue
|
|
48
|
-
return await fetchEnsFromChain(id, client, chainKeys)
|
|
49
|
-
}
|
|
50
|
-
} catch {
|
|
51
|
-
continue
|
|
52
|
-
}
|
|
53
|
-
}
|
|
66
|
+
const strategies: EnsMode[] = mode.value === 'indexer'
|
|
67
|
+
? ['indexer', 'chain']
|
|
68
|
+
: ['chain', 'indexer']
|
|
54
69
|
|
|
55
|
-
return
|
|
70
|
+
return ensCache.fetch(cacheKey.value, () =>
|
|
71
|
+
resolve(id, strategies, indexerUrls.value, $wagmi as Config, chainKeys),
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
watch: [() => toValue(identifier)],
|
|
76
|
+
getCachedData: () => ensCache.get(cacheKey.value) ?? undefined,
|
|
56
77
|
},
|
|
57
|
-
{ watch: [() => toValue(identifier)] },
|
|
58
78
|
)
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
export const useEns = (identifier: MaybeRefOrGetter<string | undefined>, options?: UseEnsOptions) =>
|
|
62
82
|
useEnsBase('resolve', identifier, [], options)
|
|
63
83
|
|
|
64
|
-
export const
|
|
84
|
+
export const useEnsWithAvatar = (identifier: MaybeRefOrGetter<string | undefined>, options?: UseEnsOptions) =>
|
|
65
85
|
useEnsBase('avatar', identifier, [...ENS_KEYS_AVATAR], options)
|
|
66
86
|
|
|
67
87
|
export const useEnsProfile = (identifier: MaybeRefOrGetter<string | undefined>, options?: UseEnsOptions) =>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
interface CacheEntry<T> {
|
|
2
|
+
data: T
|
|
3
|
+
expiresAt: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function createCache<T>(ttl: number, max: number) {
|
|
7
|
+
const entries = new Map<string, CacheEntry<T>>()
|
|
8
|
+
const pending = new Map<string, Promise<T>>()
|
|
9
|
+
|
|
10
|
+
function prune() {
|
|
11
|
+
const now = Date.now()
|
|
12
|
+
for (const [key, entry] of entries) {
|
|
13
|
+
if (entry.expiresAt <= now) entries.delete(key)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (entries.size > max) {
|
|
17
|
+
const excess = entries.size - max
|
|
18
|
+
const keys = entries.keys()
|
|
19
|
+
for (let i = 0; i < excess; i++) {
|
|
20
|
+
entries.delete(keys.next().value!)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function get(key: string): T | undefined {
|
|
26
|
+
const entry = entries.get(key)
|
|
27
|
+
if (!entry) return undefined
|
|
28
|
+
if (entry.expiresAt <= Date.now()) {
|
|
29
|
+
entries.delete(key)
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
return entry.data
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fetch(key: string, fn: () => Promise<T>): Promise<T> {
|
|
36
|
+
const cached = get(key)
|
|
37
|
+
if (cached) return Promise.resolve(cached)
|
|
38
|
+
|
|
39
|
+
const inflight = pending.get(key)
|
|
40
|
+
if (inflight) return inflight
|
|
41
|
+
|
|
42
|
+
const promise = fn()
|
|
43
|
+
.then((result) => {
|
|
44
|
+
entries.set(key, { data: result, expiresAt: Date.now() + ttl })
|
|
45
|
+
pending.delete(key)
|
|
46
|
+
if (entries.size > max) prune()
|
|
47
|
+
return result
|
|
48
|
+
})
|
|
49
|
+
.catch((err) => {
|
|
50
|
+
pending.delete(key)
|
|
51
|
+
throw err
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
pending.set(key, promise)
|
|
55
|
+
return promise
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { get, fetch }
|
|
59
|
+
}
|
package/app/utils/ens.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import { isAddress, type PublicClient, type Address } from 'viem'
|
|
2
|
+
import { normalize } from 'viem/ens'
|
|
3
|
+
|
|
4
|
+
// --- Types ---
|
|
3
5
|
|
|
4
6
|
export interface EnsProfile {
|
|
5
7
|
address: string
|
|
@@ -17,26 +19,18 @@ export interface EnsProfile {
|
|
|
17
19
|
} | null
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
// --- Text record keys ---
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
const get = (key: string) => {
|
|
24
|
-
const i = keys.indexOf(key)
|
|
25
|
-
return i >= 0 ? results[i] || '' : ''
|
|
26
|
-
}
|
|
24
|
+
const ALL_KEYS = ['avatar', 'header', 'description', 'url', 'email', 'com.twitter', 'com.github'] as const
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
github: get('com.github'),
|
|
37
|
-
},
|
|
38
|
-
}
|
|
39
|
-
}
|
|
26
|
+
export const ENS_KEYS_AVATAR = ['avatar'] as const
|
|
27
|
+
export const ENS_KEYS_PROFILE = [...ALL_KEYS]
|
|
28
|
+
|
|
29
|
+
// --- Cache ---
|
|
30
|
+
|
|
31
|
+
export const ensCache = createCache<EnsProfile>(5 * 60 * 1000, 500)
|
|
32
|
+
|
|
33
|
+
// --- Fetchers ---
|
|
40
34
|
|
|
41
35
|
export async function fetchEnsFromIndexer(
|
|
42
36
|
identifier: string,
|
|
@@ -79,11 +73,26 @@ export async function fetchEnsFromChain(
|
|
|
79
73
|
|
|
80
74
|
const name = normalize(ens)
|
|
81
75
|
const results = await Promise.all(
|
|
82
|
-
keys.map(key => client.getEnsText({ name, key }).catch(() =>
|
|
76
|
+
keys.map(key => client.getEnsText({ name, key }).catch(() => null)),
|
|
83
77
|
)
|
|
84
78
|
|
|
85
|
-
return { address, ens, data:
|
|
79
|
+
return { address, ens, data: toProfileData(keys, results.map(r => r || '')) }
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
// --- Helpers ---
|
|
83
|
+
|
|
84
|
+
function toProfileData(keys: string[], results: string[]): EnsProfile['data'] {
|
|
85
|
+
const get = (key: string) => results[keys.indexOf(key)] || ''
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
avatar: get('avatar'),
|
|
89
|
+
header: get('header'),
|
|
90
|
+
description: get('description'),
|
|
91
|
+
links: {
|
|
92
|
+
url: get('url'),
|
|
93
|
+
email: get('email'),
|
|
94
|
+
twitter: get('com.twitter'),
|
|
95
|
+
github: get('com.github'),
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|