@1001-digital/layers.evm 1.0.5 → 1.0.7

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/.env.example CHANGED
@@ -1,4 +1,9 @@
1
1
  NUXT_PUBLIC_EVM_WALLET_CONNECT_PROJECT_ID=""
2
+
2
3
  NUXT_PUBLIC_EVM_CHAINS_MAINNET_RPC1=""
3
4
  NUXT_PUBLIC_EVM_CHAINS_MAINNET_RPC2=""
4
5
  NUXT_PUBLIC_EVM_CHAINS_MAINNET_RPC3=""
6
+
7
+ NUXT_PUBLIC_EVM_ENS_INDEXER1=""
8
+ NUXT_PUBLIC_EVM_ENS_INDEXER2=""
9
+ NUXT_PUBLIC_EVM_ENS_INDEXER3=""
@@ -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<'indexer' | 'chain' | undefined>
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
- key: string,
47
+ tier: string,
10
48
  identifier: MaybeRefOrGetter<string | undefined>,
11
49
  chainKeys: string[],
12
50
  options: UseEnsOptions = {},
@@ -15,46 +53,28 @@ 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 indexerUrls = computed(() => {
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
- `ens-${key}-${toValue(identifier)}`,
61
+ cacheKey.value,
34
62
  async () => {
35
63
  const id = toValue(identifier)
36
64
  if (!id) return null
37
65
 
38
- for (const strategy of strategies.value) {
39
- try {
40
- if (strategy === 'indexer') {
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 null
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
 
@@ -0,0 +1,36 @@
1
+ import type { WatchStopHandle } from 'vue'
2
+ import { formatEther, formatGwei } from 'viem'
3
+ import { getGasPrice } from '@wagmi/core'
4
+ import { useConfig, useBlockNumber } from '@wagmi/vue'
5
+
6
+ let priceWatcher: WatchStopHandle | null = null
7
+ const price: Ref<bigint> = ref(0n)
8
+
9
+ export const useGasPrice = () => {
10
+ const config = useConfig()
11
+ const { data: blockNumber } = useBlockNumber()
12
+
13
+ const updatePrice = async () => {
14
+ price.value = await getGasPrice(config)
15
+ }
16
+
17
+ if (!priceWatcher) {
18
+ updatePrice()
19
+ priceWatcher = watch(blockNumber, () => updatePrice())
20
+ }
21
+
22
+ const unitPrice = computed(() => ({
23
+ wei: price.value,
24
+ gwei: formatGwei(price.value),
25
+ eth: formatEther(price.value),
26
+
27
+ formatted: {
28
+ gwei: price.value > 2_000_000_000_000n
29
+ ? Math.round(parseFloat(formatGwei(price.value)))
30
+ : parseFloat(formatGwei(price.value)).toFixed(1),
31
+ eth: formatEther(price.value),
32
+ },
33
+ }))
34
+
35
+ return unitPrice
36
+ }
@@ -0,0 +1,103 @@
1
+ import { readContract } from '@wagmi/core'
2
+
3
+ const CHAINLINK_ETH_USD_ABI = [
4
+ {
5
+ inputs: [],
6
+ name: 'latestRoundData',
7
+ outputs: [
8
+ { internalType: 'uint80', name: 'roundId', type: 'uint80' },
9
+ { internalType: 'int256', name: 'answer', type: 'int256' },
10
+ { internalType: 'uint256', name: 'startedAt', type: 'uint256' },
11
+ { internalType: 'uint256', name: 'updatedAt', type: 'uint256' },
12
+ { internalType: 'uint80', name: 'answeredInRound', type: 'uint80' },
13
+ ],
14
+ stateMutability: 'view',
15
+ type: 'function',
16
+ },
17
+ ] as const
18
+
19
+ const CHAINLINK_ETH_USD = '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419'
20
+ const STORAGE_KEY = 'evm:price-feed'
21
+ const CACHE_TTL = 3_600 // 1 hour in seconds
22
+
23
+ interface PriceFeedState {
24
+ ethUSDRaw: bigint | null
25
+ lastUpdated: number
26
+ }
27
+
28
+ const state = reactive<PriceFeedState>({
29
+ ethUSDRaw: null,
30
+ lastUpdated: 0,
31
+ })
32
+
33
+ function loadFromStorage() {
34
+ if (!import.meta.client) return
35
+
36
+ try {
37
+ const stored = localStorage.getItem(STORAGE_KEY)
38
+ if (!stored) return
39
+
40
+ const parsed = parseJSON(stored) as PriceFeedState
41
+ if (parsed.ethUSDRaw) state.ethUSDRaw = parsed.ethUSDRaw
42
+ if (parsed.lastUpdated) state.lastUpdated = parsed.lastUpdated
43
+ } catch {
44
+ // Ignore corrupted storage
45
+ }
46
+ }
47
+
48
+ function saveToStorage() {
49
+ if (!import.meta.client) return
50
+
51
+ try {
52
+ localStorage.setItem(STORAGE_KEY, stringifyJSON({
53
+ ethUSDRaw: state.ethUSDRaw,
54
+ lastUpdated: state.lastUpdated,
55
+ }))
56
+ } catch {
57
+ // Ignore storage errors
58
+ }
59
+ }
60
+
61
+ export const usePriceFeed = () => {
62
+ const { $wagmi } = useNuxtApp()
63
+
64
+ // Load cached data on first use
65
+ if (!state.lastUpdated) loadFromStorage()
66
+
67
+ const ethUSD = computed(() => state.ethUSDRaw ? state.ethUSDRaw / BigInt(1e8) : 0n)
68
+ const ethUSC = computed(() => state.ethUSDRaw ? state.ethUSDRaw / BigInt(1e6) : 0n)
69
+ const ethUSDFormatted = computed(() => formatPrice(Number(ethUSC.value) / 100, 2))
70
+
71
+ const weiToUSD = (wei: bigint) => {
72
+ const cents = (wei * (state.ethUSDRaw || 0n)) / (10n ** 18n) / (10n ** 6n)
73
+ return formatPrice(Number(cents) / 100, 2)
74
+ }
75
+
76
+ async function fetchPrice() {
77
+ if (nowInSeconds() - state.lastUpdated < CACHE_TTL) return
78
+
79
+ try {
80
+ const [, answer] = await readContract($wagmi, {
81
+ address: CHAINLINK_ETH_USD,
82
+ abi: CHAINLINK_ETH_USD_ABI,
83
+ functionName: 'latestRoundData',
84
+ chainId: 1,
85
+ })
86
+
87
+ state.ethUSDRaw = answer
88
+ state.lastUpdated = nowInSeconds()
89
+ saveToStorage()
90
+ } catch (error) {
91
+ console.warn('Error fetching ETH/USD price:', error)
92
+ }
93
+ }
94
+
95
+ return {
96
+ ethUSDRaw: computed(() => state.ethUSDRaw),
97
+ ethUSD,
98
+ ethUSC,
99
+ ethUSDFormatted,
100
+ weiToUSD,
101
+ fetchPrice,
102
+ }
103
+ }
@@ -0,0 +1,7 @@
1
+ export default defineNuxtPlugin(() => {
2
+ const priceFeed = usePriceFeed()
3
+
4
+ priceFeed.fetchPrice()
5
+
6
+ setInterval(() => priceFeed.fetchPrice(), 60 * 60 * 1000)
7
+ })
@@ -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,6 +1,8 @@
1
1
  import { isAddress, type PublicClient, type Address } from 'viem'
2
2
  import { normalize } from 'viem/ens'
3
3
 
4
+ // --- Types ---
5
+
4
6
  export interface EnsProfile {
5
7
  address: string
6
8
  ens: string | null
@@ -17,26 +19,18 @@ export interface EnsProfile {
17
19
  } | null
18
20
  }
19
21
 
20
- const TEXT_RECORD_KEYS = ['avatar', 'header', 'description', 'url', 'email', 'com.twitter', 'com.github'] as const
22
+ // --- Text record keys ---
21
23
 
22
- function buildData(keys: string[], results: string[]): EnsProfile['data'] {
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
- return {
29
- avatar: get('avatar'),
30
- header: get('header'),
31
- description: get('description'),
32
- links: {
33
- url: get('url'),
34
- email: get('email'),
35
- twitter: get('com.twitter'),
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,
@@ -82,8 +76,23 @@ export async function fetchEnsFromChain(
82
76
  keys.map(key => client.getEnsText({ name, key }).catch(() => null)),
83
77
  )
84
78
 
85
- return { address, ens, data: buildData(keys, results.map(r => r || '')) }
79
+ return { address, ens, data: toProfileData(keys, results.map(r => r || '')) }
86
80
  }
87
81
 
88
- export const ENS_KEYS_AVATAR = ['avatar'] as const
89
- export const ENS_KEYS_PROFILE = [...TEXT_RECORD_KEYS]
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
+ }
@@ -0,0 +1,15 @@
1
+ const replacer = (_: string, value: unknown) => {
2
+ if (typeof value === 'bigint') return value.toString() + 'n'
3
+ return value
4
+ }
5
+
6
+ const reviver = (_: string, value: unknown) => {
7
+ if (typeof value === 'string' && /^\d+n$/.test(value)) return BigInt(value.slice(0, -1))
8
+ return value
9
+ }
10
+
11
+ export const stringifyJSON = (obj: unknown): string => JSON.stringify(obj, replacer)
12
+ export const parseJSON = (json: string): unknown => JSON.parse(json, reviver)
13
+
14
+ export const formatPrice = (num: number, digits: number = 2) =>
15
+ num?.toLocaleString('en-US', { maximumFractionDigits: digits })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@1001-digital/layers.evm",
3
3
  "type": "module",
4
- "version": "1.0.5",
4
+ "version": "1.0.7",
5
5
  "main": "./nuxt.config.ts",
6
6
  "devDependencies": {
7
7
  "@nuxt/eslint": "latest",
@@ -10,10 +10,10 @@
10
10
  "nuxt": "^4.3.0",
11
11
  "typescript": "^5.9.3",
12
12
  "vue": "latest",
13
- "@1001-digital/layers.base": "^0.0.26"
13
+ "@1001-digital/layers.base": "^0.0.27"
14
14
  },
15
15
  "peerDependencies": {
16
- "@1001-digital/layers.base": "^0.0.26"
16
+ "@1001-digital/layers.base": "^0.0.27"
17
17
  },
18
18
  "dependencies": {
19
19
  "@types/qrcode": "^1.5.6",