@1001-digital/layers.evm 1.0.6 → 1.0.8
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/.claude/settings.local.json +1 -3
- package/.env.example +5 -0
- package/.playground/app/app.vue +1 -0
- package/.playground/app/pages/index.vue +1 -2
- package/.playground/nuxt.config.ts +3 -1
- package/AGENTS.md +2 -2
- package/README.md +1 -1
- package/app/composables/base.ts +1 -5
- package/app/composables/chainId.ts +6 -42
- package/app/plugins/wagmi.ts +40 -3
- package/app/utils/addresses.ts +1 -4
- package/app/utils/chains.ts +1 -13
- package/app/utils/format-eth.ts +1 -12
- package/nuxt.config.ts +33 -0
- package/package.json +5 -6
- package/app/components/EvmAccount.client.vue +0 -26
- package/app/components/EvmConnect.client.vue +0 -238
- package/app/components/EvmConnectorQR.client.vue +0 -108
- package/app/components/EvmMetaMaskQR.client.vue +0 -13
- package/app/components/EvmTransactionFlow.vue +0 -293
- package/app/components/EvmWalletConnectQR.client.vue +0 -13
- package/app/composables/clipboard.ts +0 -26
- package/app/composables/ens.ts +0 -88
- package/app/composables/helpers.ts +0 -1
- package/app/utils/cache.ts +0 -59
- package/app/utils/ens.ts +0 -98
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=""
|
package/.playground/app/app.vue
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url'
|
|
2
2
|
|
|
3
3
|
const layerDir = fileURLToPath(new URL('..', import.meta.url))
|
|
4
|
+
const componentsDir = fileURLToPath(new URL('../../components/src', import.meta.url))
|
|
4
5
|
|
|
5
6
|
export default defineNuxtConfig({
|
|
6
7
|
extends: ['@1001-digital/layers.base', '..'],
|
|
@@ -9,11 +10,12 @@ export default defineNuxtConfig({
|
|
|
9
10
|
config: {
|
|
10
11
|
// Use the generated ESLint config for lint root project as well
|
|
11
12
|
rootDir: layerDir,
|
|
12
|
-
}
|
|
13
|
+
},
|
|
13
14
|
},
|
|
14
15
|
hooks: {
|
|
15
16
|
'vite:serverCreated': (server) => {
|
|
16
17
|
server.watcher.add(layerDir)
|
|
18
|
+
server.watcher.add(componentsDir)
|
|
17
19
|
},
|
|
18
20
|
},
|
|
19
21
|
})
|
package/AGENTS.md
CHANGED
|
@@ -18,6 +18,7 @@ Nuxt layer for building dAPPs (Ethereum-powered applications). Extends `@1001-di
|
|
|
18
18
|
## Wagmi Configuration
|
|
19
19
|
|
|
20
20
|
Uses modern wagmi 0.4.x patterns:
|
|
21
|
+
|
|
21
22
|
- `useConnection` (not deprecated `useAccount`)
|
|
22
23
|
- `useConnectionEffect` (not deprecated `useAccountEffect`)
|
|
23
24
|
- `useSwitchConnection` (not deprecated `useSwitchAccount`)
|
|
@@ -29,7 +30,7 @@ Connectors: injected, coinbaseWallet, metaMask, walletConnect
|
|
|
29
30
|
## Components
|
|
30
31
|
|
|
31
32
|
- `EvmConnect.client.vue` - Wallet connection button with modal
|
|
32
|
-
- `EvmAccount.client.vue` - Address display
|
|
33
|
+
- `EvmAccount.client.vue` - Address display
|
|
33
34
|
- `EvmTransactionFlow.vue` - Guided transaction execution flow
|
|
34
35
|
- `EvmConnectorQR.client.vue` - Base QR code renderer
|
|
35
36
|
- `EvmWalletConnectQR.client.vue` - WalletConnect QR wrapper
|
|
@@ -42,7 +43,6 @@ Connectors: injected, coinbaseWallet, metaMask, walletConnect
|
|
|
42
43
|
- `useBlockExplorer(key?)` - Get block explorer URL for a named chain
|
|
43
44
|
- `useEnsureChainIdCheck()` - Validate/switch chain before transactions
|
|
44
45
|
- `useBaseURL()` - Get base URL with trailing slash
|
|
45
|
-
- `useClipboard()` - Copy text to clipboard with copied state
|
|
46
46
|
|
|
47
47
|
## Utilities
|
|
48
48
|
|
package/README.md
CHANGED
package/app/composables/base.ts
CHANGED
|
@@ -1,42 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const getDefaultChainKey = () => useAppConfig().evm?.defaultChain || 'mainnet'
|
|
9
|
-
|
|
10
|
-
export const useChainConfig = (key?: string) => {
|
|
11
|
-
const appConfig = useAppConfig()
|
|
12
|
-
const resolvedKey = key || getDefaultChainKey()
|
|
13
|
-
const chains = appConfig.evm?.chains as Record<string, ChainConfig> | undefined
|
|
14
|
-
const chain = chains?.[resolvedKey]
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
id: chain?.id ?? 1,
|
|
18
|
-
blockExplorer: chain?.blockExplorer ?? 'https://etherscan.io',
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const useMainChainId = () => useChainConfig().id
|
|
23
|
-
|
|
24
|
-
export const useBlockExplorer = (key?: string) => useChainConfig(key).blockExplorer
|
|
25
|
-
|
|
26
|
-
export const useEnsureChainIdCheck = () => {
|
|
27
|
-
const chainId = useMainChainId()
|
|
28
|
-
const { switchChain } = useSwitchChain()
|
|
29
|
-
const { chainId: currentChainId } = useConnection()
|
|
30
|
-
|
|
31
|
-
return async () => {
|
|
32
|
-
if (chainId !== currentChainId.value) {
|
|
33
|
-
switchChain({ chainId })
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (chainId === currentChainId.value) {
|
|
37
|
-
return true
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
useChainConfig,
|
|
3
|
+
useMainChainId,
|
|
4
|
+
useBlockExplorer,
|
|
5
|
+
useEnsureChainIdCheck,
|
|
6
|
+
} from '@1001-digital/components'
|
package/app/plugins/wagmi.ts
CHANGED
|
@@ -9,14 +9,25 @@ import {
|
|
|
9
9
|
type Config,
|
|
10
10
|
type CreateConnectorFn,
|
|
11
11
|
} from '@wagmi/vue'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
coinbaseWallet,
|
|
14
|
+
injected,
|
|
15
|
+
metaMask,
|
|
16
|
+
walletConnect,
|
|
17
|
+
} from '@wagmi/vue/connectors'
|
|
13
18
|
import type { Chain, Transport } from 'viem'
|
|
19
|
+
import {
|
|
20
|
+
EvmConfigKey,
|
|
21
|
+
resolveChain,
|
|
22
|
+
type EvmConfig,
|
|
23
|
+
} from '@1001-digital/components'
|
|
14
24
|
|
|
15
25
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
16
26
|
const appConfig = useAppConfig()
|
|
17
27
|
const runtimeConfig = nuxtApp.$config.public.evm as {
|
|
18
28
|
walletConnectProjectId: string
|
|
19
|
-
chains: Record<string, { rpc1?: string
|
|
29
|
+
chains: Record<string, { rpc1?: string; rpc2?: string; rpc3?: string }>
|
|
30
|
+
ens: { indexer1?: string; indexer2?: string; indexer3?: string }
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
const title = appConfig.evm?.title || 'EVM Layer'
|
|
@@ -78,7 +89,33 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
78
89
|
transports,
|
|
79
90
|
})
|
|
80
91
|
|
|
81
|
-
|
|
92
|
+
// Build EvmConfig from Nuxt app/runtime config
|
|
93
|
+
const indexerUrls = [
|
|
94
|
+
runtimeConfig.ens?.indexer1,
|
|
95
|
+
runtimeConfig.ens?.indexer2,
|
|
96
|
+
runtimeConfig.ens?.indexer3,
|
|
97
|
+
].filter(Boolean) as string[]
|
|
98
|
+
|
|
99
|
+
const evmConfig: EvmConfig = {
|
|
100
|
+
title,
|
|
101
|
+
defaultChain: appConfig.evm?.defaultChain || 'mainnet',
|
|
102
|
+
chains: Object.fromEntries(
|
|
103
|
+
Object.entries(chainEntries).map(([key, entry]) => [
|
|
104
|
+
key,
|
|
105
|
+
{ id: entry.id!, blockExplorer: entry.blockExplorer },
|
|
106
|
+
]),
|
|
107
|
+
),
|
|
108
|
+
ens: {
|
|
109
|
+
mode: appConfig.evm?.ens?.mode || 'indexer',
|
|
110
|
+
indexerUrls,
|
|
111
|
+
},
|
|
112
|
+
baseURL: nuxtApp.$config.app.baseURL,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
nuxtApp.vueApp
|
|
116
|
+
.use(WagmiPlugin, { config: wagmiConfig })
|
|
117
|
+
.use(VueQueryPlugin, {})
|
|
118
|
+
.provide(EvmConfigKey, evmConfig)
|
|
82
119
|
|
|
83
120
|
return {
|
|
84
121
|
provide: {
|
package/app/utils/addresses.ts
CHANGED
package/app/utils/chains.ts
CHANGED
|
@@ -1,13 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { mainnet, sepolia, holesky, optimism, arbitrum, base, polygon, localhost } from 'viem/chains'
|
|
3
|
-
|
|
4
|
-
const KNOWN: Chain[] = [mainnet, sepolia, holesky, optimism, arbitrum, base, polygon, localhost]
|
|
5
|
-
const byId = new Map<number, Chain>(KNOWN.map(c => [c.id, c]))
|
|
6
|
-
|
|
7
|
-
export const resolveChain = (id: number): Chain =>
|
|
8
|
-
byId.get(id) ?? defineChain({
|
|
9
|
-
id,
|
|
10
|
-
name: `Chain ${id}`,
|
|
11
|
-
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
|
12
|
-
rpcUrls: { default: { http: [] } },
|
|
13
|
-
})
|
|
1
|
+
export { resolveChain } from '@1001-digital/components'
|
package/app/utils/format-eth.ts
CHANGED
|
@@ -1,12 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
const numberValue = typeof value === 'string' ? parseFloat(value) : value
|
|
3
|
-
|
|
4
|
-
if (isNaN(numberValue)) {
|
|
5
|
-
throw new Error('Invalid number input')
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
return new Intl.NumberFormat('en-US', {
|
|
9
|
-
minimumFractionDigits: 0,
|
|
10
|
-
maximumFractionDigits: maxDecimals,
|
|
11
|
-
}).format(numberValue)
|
|
12
|
-
}
|
|
1
|
+
export { formatETH } from '@1001-digital/components'
|
package/nuxt.config.ts
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
|
|
3
|
+
const componentsDir = fileURLToPath(
|
|
4
|
+
new URL('../components/src/evm/components', import.meta.url),
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
const clientOnlyComponents = [
|
|
8
|
+
'EvmAccount',
|
|
9
|
+
'EvmConnect',
|
|
10
|
+
'EvmConnectorQR',
|
|
11
|
+
'EvmMetaMaskQR',
|
|
12
|
+
'EvmWalletConnectQR',
|
|
13
|
+
]
|
|
14
|
+
|
|
1
15
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
2
16
|
export default defineNuxtConfig({
|
|
3
17
|
extends: ['@1001-digital/layers.base'],
|
|
4
18
|
|
|
5
19
|
modules: ['@wagmi/vue/nuxt'],
|
|
6
20
|
|
|
21
|
+
hooks: {
|
|
22
|
+
'components:dirs': (dirs) => {
|
|
23
|
+
dirs.push({
|
|
24
|
+
path: componentsDir,
|
|
25
|
+
pathPrefix: false,
|
|
26
|
+
})
|
|
27
|
+
},
|
|
28
|
+
'components:extend': (components) => {
|
|
29
|
+
for (const c of components) {
|
|
30
|
+
if (clientOnlyComponents.includes(c.pascalName)) {
|
|
31
|
+
c.mode = 'client'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
|
|
7
37
|
ssr: process.env.NUXT_SSR !== 'false',
|
|
8
38
|
|
|
9
39
|
runtimeConfig: {
|
|
@@ -23,6 +53,9 @@ export default defineNuxtConfig({
|
|
|
23
53
|
},
|
|
24
54
|
|
|
25
55
|
vite: {
|
|
56
|
+
resolve: {
|
|
57
|
+
dedupe: ['@wagmi/vue', '@wagmi/core', 'viem'],
|
|
58
|
+
},
|
|
26
59
|
optimizeDeps: {
|
|
27
60
|
include: [
|
|
28
61
|
'@1001-digital/layers.evm > @metamask/sdk',
|
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.
|
|
4
|
+
"version": "1.0.8",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"@nuxt/eslint": "latest",
|
|
@@ -10,20 +10,19 @@
|
|
|
10
10
|
"nuxt": "^4.3.0",
|
|
11
11
|
"typescript": "^5.9.3",
|
|
12
12
|
"vue": "latest",
|
|
13
|
-
"@1001-digital/layers.base": "^0.0.
|
|
13
|
+
"@1001-digital/layers.base": "^0.0.32"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"@1001-digital/layers.base": "^0.0.
|
|
16
|
+
"@1001-digital/layers.base": "^0.0.32"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@types/qrcode": "^1.5.6",
|
|
20
19
|
"@metamask/sdk": "~0.34.0",
|
|
21
20
|
"@tanstack/vue-query": "^5.92.9",
|
|
22
21
|
"@wagmi/core": "^3.3.2",
|
|
23
22
|
"@wagmi/vue": "^0.4.15",
|
|
24
23
|
"@walletconnect/ethereum-provider": "~2.23.4",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
24
|
+
"viem": "~2.45.1",
|
|
25
|
+
"@1001-digital/components": "^0.0.1"
|
|
27
26
|
},
|
|
28
27
|
"scripts": {
|
|
29
28
|
"dev": "nuxi dev .playground",
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<slot :display="display" :is-current="isCurrent">
|
|
3
|
-
<span>{{ display }}</span>
|
|
4
|
-
</slot>
|
|
5
|
-
</template>
|
|
6
|
-
|
|
7
|
-
<script setup lang="ts">
|
|
8
|
-
import type { Address } from 'viem'
|
|
9
|
-
import { useConnection } from '@wagmi/vue'
|
|
10
|
-
|
|
11
|
-
const props = defineProps<{
|
|
12
|
-
address?: Address
|
|
13
|
-
mode?: 'indexer' | 'chain'
|
|
14
|
-
}>()
|
|
15
|
-
const address = computed(() => props.address)
|
|
16
|
-
|
|
17
|
-
const { address: currentAddress } = useConnection()
|
|
18
|
-
|
|
19
|
-
const isCurrent = computed<boolean>(
|
|
20
|
-
() => currentAddress.value?.toLowerCase() === address.value?.toLowerCase(),
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
const { data: profile } = useEns(address, { mode: computed(() => props.mode) })
|
|
24
|
-
|
|
25
|
-
const display = computed<string>(() => profile.value?.ens || shortAddress(address.value!))
|
|
26
|
-
</script>
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<Button
|
|
3
|
-
v-if="showConnect"
|
|
4
|
-
@click="chooseModalOpen = true"
|
|
5
|
-
:class="className"
|
|
6
|
-
>
|
|
7
|
-
<slot>Connect Wallet</slot>
|
|
8
|
-
</Button>
|
|
9
|
-
<slot
|
|
10
|
-
v-else
|
|
11
|
-
name="connected"
|
|
12
|
-
:address="address"
|
|
13
|
-
>
|
|
14
|
-
<EvmAccount :address="address" />
|
|
15
|
-
</slot>
|
|
16
|
-
|
|
17
|
-
<Dialog
|
|
18
|
-
v-if="showConnect"
|
|
19
|
-
title="Connect Wallet"
|
|
20
|
-
v-model:open="chooseModalOpen"
|
|
21
|
-
@closed="onModalClosed"
|
|
22
|
-
>
|
|
23
|
-
<Alert
|
|
24
|
-
v-if="errorMessage"
|
|
25
|
-
type="error"
|
|
26
|
-
>
|
|
27
|
-
{{ errorMessage }}
|
|
28
|
-
</Alert>
|
|
29
|
-
<EvmWalletConnectQR
|
|
30
|
-
v-if="walletConnectUri"
|
|
31
|
-
:uri="walletConnectUri"
|
|
32
|
-
/>
|
|
33
|
-
<EvmMetaMaskQR
|
|
34
|
-
v-else-if="metaMaskUri"
|
|
35
|
-
:uri="metaMaskUri"
|
|
36
|
-
/>
|
|
37
|
-
<template v-else-if="isConnecting">
|
|
38
|
-
<Loading
|
|
39
|
-
txt="Waiting for wallet confirmation..."
|
|
40
|
-
spinner
|
|
41
|
-
stacked
|
|
42
|
-
/>
|
|
43
|
-
</template>
|
|
44
|
-
<div
|
|
45
|
-
v-else
|
|
46
|
-
class="wallet-options"
|
|
47
|
-
>
|
|
48
|
-
<Button
|
|
49
|
-
v-for="connector in shownConnectors"
|
|
50
|
-
:key="connector.uid"
|
|
51
|
-
@click="() => login(connector)"
|
|
52
|
-
class="choose-connector"
|
|
53
|
-
>
|
|
54
|
-
<img
|
|
55
|
-
v-if="ICONS[connector.name]"
|
|
56
|
-
:src="
|
|
57
|
-
connector.icon || `${base}icons/wallets/${ICONS[connector.name]}`
|
|
58
|
-
"
|
|
59
|
-
:alt="connector.name"
|
|
60
|
-
/>
|
|
61
|
-
<div
|
|
62
|
-
v-else
|
|
63
|
-
class="default-wallet-icon"
|
|
64
|
-
>
|
|
65
|
-
<Icon type="wallet" />
|
|
66
|
-
</div>
|
|
67
|
-
<span>{{ connector.name }}</span>
|
|
68
|
-
</Button>
|
|
69
|
-
<Button
|
|
70
|
-
to="https://ethereum.org/wallets/"
|
|
71
|
-
target="_blank"
|
|
72
|
-
class="link muted small"
|
|
73
|
-
>
|
|
74
|
-
<Icon type="help" />
|
|
75
|
-
<span>New to wallets?</span>
|
|
76
|
-
</Button>
|
|
77
|
-
</div>
|
|
78
|
-
</Dialog>
|
|
79
|
-
</template>
|
|
80
|
-
|
|
81
|
-
<script setup lang="ts">
|
|
82
|
-
import type { Connector } from '@wagmi/vue'
|
|
83
|
-
import { useConnection, useConnect, useChainId } from '@wagmi/vue'
|
|
84
|
-
|
|
85
|
-
const ICONS: Record<string, string> = {
|
|
86
|
-
'Coinbase Wallet': 'coinbase.svg',
|
|
87
|
-
MetaMask: 'metamask.svg',
|
|
88
|
-
Phantom: 'phantom.svg',
|
|
89
|
-
'Rabby Wallet': 'rabby.svg',
|
|
90
|
-
Rainbow: 'rainbow.svg',
|
|
91
|
-
WalletConnect: 'walletconnect.svg',
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const PRIORITY: Record<string, number> = {
|
|
95
|
-
WalletConnect: 20,
|
|
96
|
-
'Coinbase Wallet': 10,
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
defineProps<{
|
|
100
|
-
className?: string
|
|
101
|
-
}>()
|
|
102
|
-
const emit = defineEmits<{
|
|
103
|
-
connected: [{ address: `0x${string}` | undefined }]
|
|
104
|
-
disconnected: []
|
|
105
|
-
}>()
|
|
106
|
-
const base = useBaseURL()
|
|
107
|
-
|
|
108
|
-
const chainId = useChainId()
|
|
109
|
-
const { connectors, connectAsync } = useConnect()
|
|
110
|
-
const { address, isConnected } = useConnection()
|
|
111
|
-
|
|
112
|
-
const showConnect = computed(() => !isConnected.value)
|
|
113
|
-
const shownConnectors = computed(() => {
|
|
114
|
-
const unique = Array.from(
|
|
115
|
-
new Map(
|
|
116
|
-
connectors?.map((connector) => [connector.name, connector]),
|
|
117
|
-
).values(),
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
const filtered =
|
|
121
|
-
unique.length > 1 ? unique.filter((c) => c.id !== 'injected') : unique
|
|
122
|
-
|
|
123
|
-
return filtered.sort((a, b) => {
|
|
124
|
-
const priorityA = PRIORITY[a.name] ?? 5
|
|
125
|
-
const priorityB = PRIORITY[b.name] ?? 5
|
|
126
|
-
return priorityA - priorityB
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
const chooseModalOpen = ref(false)
|
|
131
|
-
const errorMessage = ref('')
|
|
132
|
-
const isConnecting = ref(false)
|
|
133
|
-
const walletConnectUri = ref('')
|
|
134
|
-
const metaMaskUri = ref('')
|
|
135
|
-
|
|
136
|
-
const login = async (connector: Connector) => {
|
|
137
|
-
errorMessage.value = ''
|
|
138
|
-
isConnecting.value = true
|
|
139
|
-
walletConnectUri.value = ''
|
|
140
|
-
metaMaskUri.value = ''
|
|
141
|
-
|
|
142
|
-
const handleMessage = (event: { type: string; data?: unknown }) => {
|
|
143
|
-
if (event.type === 'display_uri' && typeof event.data === 'string') {
|
|
144
|
-
if (connector.id === 'walletConnect') {
|
|
145
|
-
walletConnectUri.value = event.data
|
|
146
|
-
} else if (connector.id === 'metaMaskSDK') {
|
|
147
|
-
metaMaskUri.value = event.data
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (connector.id === 'walletConnect' || connector.id === 'metaMaskSDK') {
|
|
153
|
-
connector.emitter.on('message', handleMessage)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
await connectAsync({ connector, chainId: chainId.value })
|
|
158
|
-
|
|
159
|
-
setTimeout(() => {
|
|
160
|
-
chooseModalOpen.value = false
|
|
161
|
-
isConnecting.value = false
|
|
162
|
-
walletConnectUri.value = ''
|
|
163
|
-
metaMaskUri.value = ''
|
|
164
|
-
}, 100)
|
|
165
|
-
} catch (error: unknown) {
|
|
166
|
-
isConnecting.value = false
|
|
167
|
-
walletConnectUri.value = ''
|
|
168
|
-
metaMaskUri.value = ''
|
|
169
|
-
|
|
170
|
-
const errorMsg = error instanceof Error ? error.message : ''
|
|
171
|
-
if (
|
|
172
|
-
errorMsg.includes('User rejected') ||
|
|
173
|
-
errorMsg.includes('rejected') ||
|
|
174
|
-
errorMsg.includes('denied')
|
|
175
|
-
) {
|
|
176
|
-
errorMessage.value = 'Connection cancelled. Please try again.'
|
|
177
|
-
} else {
|
|
178
|
-
errorMessage.value = 'Failed to connect. Please try again.'
|
|
179
|
-
}
|
|
180
|
-
console.error('Wallet connection error:', error)
|
|
181
|
-
} finally {
|
|
182
|
-
if (connector.id === 'walletConnect' || connector.id === 'metaMaskSDK') {
|
|
183
|
-
connector.emitter.off('message', handleMessage)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const onModalClosed = () => {
|
|
189
|
-
errorMessage.value = ''
|
|
190
|
-
isConnecting.value = false
|
|
191
|
-
walletConnectUri.value = ''
|
|
192
|
-
metaMaskUri.value = ''
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const check = () =>
|
|
196
|
-
isConnected.value
|
|
197
|
-
? emit('connected', { address: address.value })
|
|
198
|
-
: emit('disconnected')
|
|
199
|
-
watch(isConnected, () => check())
|
|
200
|
-
onMounted(() => check())
|
|
201
|
-
</script>
|
|
202
|
-
|
|
203
|
-
<style scoped>
|
|
204
|
-
.wallet-options {
|
|
205
|
-
display: grid;
|
|
206
|
-
gap: var(--spacer);
|
|
207
|
-
|
|
208
|
-
button.choose-connector {
|
|
209
|
-
width: 100%;
|
|
210
|
-
inline-size: auto;
|
|
211
|
-
justify-content: flex-start;
|
|
212
|
-
|
|
213
|
-
img,
|
|
214
|
-
.default-wallet-icon {
|
|
215
|
-
margin: -1rem 0 -1rem -0.6rem;
|
|
216
|
-
width: var(--size-5);
|
|
217
|
-
height: var(--size-5);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
.default-wallet-icon {
|
|
221
|
-
display: flex;
|
|
222
|
-
align-items: center;
|
|
223
|
-
justify-content: center;
|
|
224
|
-
background: var(--gray-z-2);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
span:last-child {
|
|
228
|
-
border-left: var(--border);
|
|
229
|
-
padding-left: var(--spacer-sm);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
.link.muted {
|
|
235
|
-
justify-self: center;
|
|
236
|
-
font-size: var(--font-xs);
|
|
237
|
-
}
|
|
238
|
-
</style>
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<p>
|
|
3
|
-
<slot name="instruction">Scan the code in your wallet application</slot>
|
|
4
|
-
</p>
|
|
5
|
-
<div class="qr-frame">
|
|
6
|
-
<canvas ref="qrCanvas"></canvas>
|
|
7
|
-
</div>
|
|
8
|
-
<p class="uri-label">Or copy the connection URI:</p>
|
|
9
|
-
<div class="uri-display">
|
|
10
|
-
<code>{{ uri }}</code>
|
|
11
|
-
<Button @click="copyUri" class="copy-button" :class="{ copied: isCopied }">
|
|
12
|
-
<Icon :type="isCopied ? 'check' : 'copy'" />
|
|
13
|
-
</Button>
|
|
14
|
-
</div>
|
|
15
|
-
</template>
|
|
16
|
-
|
|
17
|
-
<script setup lang="ts">
|
|
18
|
-
import QRCode from 'qrcode'
|
|
19
|
-
|
|
20
|
-
const props = defineProps<{
|
|
21
|
-
uri: string
|
|
22
|
-
}>()
|
|
23
|
-
|
|
24
|
-
const qrCanvas = ref<HTMLCanvasElement | null>(null)
|
|
25
|
-
const { copy, copied: isCopied } = useClipboard()
|
|
26
|
-
|
|
27
|
-
const generateQR = async () => {
|
|
28
|
-
if (!qrCanvas.value || !props.uri) return
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
await QRCode.toCanvas(qrCanvas.value, props.uri, {
|
|
32
|
-
width: 300,
|
|
33
|
-
margin: 2,
|
|
34
|
-
color: {
|
|
35
|
-
dark: '#000000',
|
|
36
|
-
light: '#FFFFFF',
|
|
37
|
-
},
|
|
38
|
-
})
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.error('Failed to generate QR code:', error)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const copyUri = () => copy(props.uri)
|
|
45
|
-
|
|
46
|
-
watch(() => props.uri, generateQR, { immediate: true })
|
|
47
|
-
|
|
48
|
-
onMounted(() => {
|
|
49
|
-
generateQR()
|
|
50
|
-
})
|
|
51
|
-
</script>
|
|
52
|
-
|
|
53
|
-
<style scoped>
|
|
54
|
-
p {
|
|
55
|
-
text-align: center;
|
|
56
|
-
@mixin ui-font;
|
|
57
|
-
color: var(--muted);
|
|
58
|
-
font-size: var(--font-sm);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.qr-frame {
|
|
62
|
-
background: white;
|
|
63
|
-
padding: var(--spacer-sm);
|
|
64
|
-
max-width: 15rem;
|
|
65
|
-
max-height: 15rem;
|
|
66
|
-
border: var(--border);
|
|
67
|
-
border-radius: var(--border-radius);
|
|
68
|
-
margin: 0 auto;
|
|
69
|
-
|
|
70
|
-
canvas {
|
|
71
|
-
width: 100% !important;
|
|
72
|
-
height: 100% !important;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.uri-display {
|
|
77
|
-
display: flex;
|
|
78
|
-
align-items: center;
|
|
79
|
-
gap: var(--spacer-xs);
|
|
80
|
-
background: var(--color-bg-secondary);
|
|
81
|
-
border: var(--border);
|
|
82
|
-
border-radius: var(--border-radius-sm);
|
|
83
|
-
overflow: hidden;
|
|
84
|
-
height: min-content;
|
|
85
|
-
padding: 0;
|
|
86
|
-
|
|
87
|
-
code {
|
|
88
|
-
flex: 1;
|
|
89
|
-
font-size: var(--font-xs);
|
|
90
|
-
font-family: monospace;
|
|
91
|
-
white-space: nowrap;
|
|
92
|
-
overflow: hidden;
|
|
93
|
-
padding: 0 var(--spacer-sm);
|
|
94
|
-
color: var(--muted);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.copy-button {
|
|
98
|
-
flex-shrink: 0;
|
|
99
|
-
padding: var(--spacer-xs);
|
|
100
|
-
min-width: auto;
|
|
101
|
-
margin: -1px;
|
|
102
|
-
|
|
103
|
-
&.copied {
|
|
104
|
-
color: var(--color-success);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
</style>
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<slot
|
|
3
|
-
:start="start"
|
|
4
|
-
:step="step"
|
|
5
|
-
:open="open"
|
|
6
|
-
name="start"
|
|
7
|
-
></slot>
|
|
8
|
-
|
|
9
|
-
<Dialog
|
|
10
|
-
v-model:open="open"
|
|
11
|
-
:closable="canDismiss"
|
|
12
|
-
:click-outside="canDismiss"
|
|
13
|
-
:title="text.title[step]"
|
|
14
|
-
class="transaction-flow"
|
|
15
|
-
>
|
|
16
|
-
<slot name="before" />
|
|
17
|
-
|
|
18
|
-
<Loading
|
|
19
|
-
v-if="step === 'requesting' || step === 'waiting'"
|
|
20
|
-
spinner
|
|
21
|
-
stacked
|
|
22
|
-
:txt="text.lead[step] || ''"
|
|
23
|
-
/>
|
|
24
|
-
|
|
25
|
-
<p
|
|
26
|
-
v-if="
|
|
27
|
-
step !== 'requesting' &&
|
|
28
|
-
step !== 'waiting' &&
|
|
29
|
-
step !== 'error' &&
|
|
30
|
-
text.lead[step]
|
|
31
|
-
"
|
|
32
|
-
>
|
|
33
|
-
{{ text.lead[step] }}
|
|
34
|
-
</p>
|
|
35
|
-
|
|
36
|
-
<Alert
|
|
37
|
-
v-if="error"
|
|
38
|
-
type="error"
|
|
39
|
-
>
|
|
40
|
-
<p v-if="text.lead[step]">{{ text.lead[step] }}</p>
|
|
41
|
-
<p>{{ error }}</p>
|
|
42
|
-
</Alert>
|
|
43
|
-
|
|
44
|
-
<Button
|
|
45
|
-
v-if="step === 'waiting'"
|
|
46
|
-
:to="txLink"
|
|
47
|
-
target="_blank"
|
|
48
|
-
class="link muted small centered"
|
|
49
|
-
>
|
|
50
|
-
<Icon type="link" />
|
|
51
|
-
<span>View on Block Explorer</span>
|
|
52
|
-
</Button>
|
|
53
|
-
|
|
54
|
-
<slot
|
|
55
|
-
:name="step"
|
|
56
|
-
:cancel="cancel"
|
|
57
|
-
></slot>
|
|
58
|
-
|
|
59
|
-
<template #footer>
|
|
60
|
-
<template v-if="step === 'chain'">
|
|
61
|
-
<Button
|
|
62
|
-
@click="cancel"
|
|
63
|
-
class="secondary"
|
|
64
|
-
>Cancel</Button
|
|
65
|
-
>
|
|
66
|
-
</template>
|
|
67
|
-
|
|
68
|
-
<template v-if="step === 'confirm' || step === 'error'">
|
|
69
|
-
<Button
|
|
70
|
-
@click="cancel"
|
|
71
|
-
class="secondary"
|
|
72
|
-
>Cancel</Button
|
|
73
|
-
>
|
|
74
|
-
<Button @click="() => initializeRequest()">
|
|
75
|
-
{{ text.action[step] || 'Execute' }}
|
|
76
|
-
</Button>
|
|
77
|
-
</template>
|
|
78
|
-
|
|
79
|
-
<slot
|
|
80
|
-
name="actions"
|
|
81
|
-
:step="step"
|
|
82
|
-
:cancel="cancel"
|
|
83
|
-
:execute="() => initializeRequest()"
|
|
84
|
-
:tx-link="txLink"
|
|
85
|
-
/>
|
|
86
|
-
</template>
|
|
87
|
-
</Dialog>
|
|
88
|
-
</template>
|
|
89
|
-
|
|
90
|
-
<script setup lang="ts">
|
|
91
|
-
import { waitForTransactionReceipt, watchChainId } from '@wagmi/core'
|
|
92
|
-
import type { Config } from '@wagmi/vue'
|
|
93
|
-
import type { TransactionReceipt, Hash } from 'viem'
|
|
94
|
-
|
|
95
|
-
interface TextConfig {
|
|
96
|
-
title?: Record<string, string>
|
|
97
|
-
lead?: Record<string, string>
|
|
98
|
-
action?: Record<string, string>
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
type Step =
|
|
102
|
-
| 'idle'
|
|
103
|
-
| 'confirm'
|
|
104
|
-
| 'chain'
|
|
105
|
-
| 'requesting'
|
|
106
|
-
| 'waiting'
|
|
107
|
-
| 'complete'
|
|
108
|
-
| 'error'
|
|
109
|
-
|
|
110
|
-
const defaultText = {
|
|
111
|
-
title: {
|
|
112
|
-
confirm: 'Confirm Transaction',
|
|
113
|
-
chain: 'Switch Network',
|
|
114
|
-
requesting: 'Requesting',
|
|
115
|
-
waiting: 'Processing',
|
|
116
|
-
complete: 'Complete',
|
|
117
|
-
error: 'Error',
|
|
118
|
-
},
|
|
119
|
-
lead: {
|
|
120
|
-
confirm: 'Please review and confirm this transaction.',
|
|
121
|
-
chain: 'Please switch to the correct network to continue.',
|
|
122
|
-
requesting: 'Requesting transaction signature...',
|
|
123
|
-
waiting: 'Waiting for transaction confirmation...',
|
|
124
|
-
complete: 'Transaction confirmed successfully.',
|
|
125
|
-
},
|
|
126
|
-
action: {
|
|
127
|
-
confirm: 'Execute',
|
|
128
|
-
error: 'Try Again',
|
|
129
|
-
},
|
|
130
|
-
} satisfies TextConfig
|
|
131
|
-
|
|
132
|
-
const slots = useSlots()
|
|
133
|
-
const checkChain = useEnsureChainIdCheck()
|
|
134
|
-
|
|
135
|
-
const { $wagmi } = useNuxtApp()
|
|
136
|
-
const blockExplorer = useBlockExplorer()
|
|
137
|
-
|
|
138
|
-
const props = withDefaults(
|
|
139
|
-
defineProps<{
|
|
140
|
-
text?: TextConfig
|
|
141
|
-
request?: () => Promise<Hash>
|
|
142
|
-
delayAfter?: number
|
|
143
|
-
delayAutoclose?: number
|
|
144
|
-
skipConfirmation?: boolean
|
|
145
|
-
autoCloseSuccess?: boolean
|
|
146
|
-
dismissable?: boolean
|
|
147
|
-
}>(),
|
|
148
|
-
{
|
|
149
|
-
delayAfter: 2000,
|
|
150
|
-
delayAutoclose: 2000,
|
|
151
|
-
skipConfirmation: false,
|
|
152
|
-
autoCloseSuccess: true,
|
|
153
|
-
dismissable: true,
|
|
154
|
-
},
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
const emit = defineEmits<{
|
|
158
|
-
complete: [receipt: TransactionReceipt]
|
|
159
|
-
cancel: []
|
|
160
|
-
}>()
|
|
161
|
-
|
|
162
|
-
const text = computed<Required<TextConfig>>(() => ({
|
|
163
|
-
title: { ...defaultText.title, ...props.text?.title },
|
|
164
|
-
lead: { ...defaultText.lead, ...props.text?.lead },
|
|
165
|
-
action: { ...defaultText.action, ...props.text?.action },
|
|
166
|
-
}))
|
|
167
|
-
|
|
168
|
-
const step = ref<Step>('idle')
|
|
169
|
-
|
|
170
|
-
const open = computed({
|
|
171
|
-
get: () => step.value !== 'idle',
|
|
172
|
-
set: (v) => {
|
|
173
|
-
if (!v) {
|
|
174
|
-
step.value = 'idle'
|
|
175
|
-
error.value = ''
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
watchChainId($wagmi as Config, {
|
|
181
|
-
async onChange() {
|
|
182
|
-
if (step.value !== 'chain') return
|
|
183
|
-
|
|
184
|
-
if (await checkChain()) {
|
|
185
|
-
initializeRequest()
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const cachedRequest = ref(props.request)
|
|
191
|
-
watch(
|
|
192
|
-
() => props.request,
|
|
193
|
-
(v) => {
|
|
194
|
-
cachedRequest.value = v
|
|
195
|
-
},
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
const error = ref('')
|
|
199
|
-
const tx = ref<Hash | null>(null)
|
|
200
|
-
const receipt = ref<TransactionReceipt | null>(null)
|
|
201
|
-
const txLink = computed(() => `${blockExplorer}/tx/${tx.value}`)
|
|
202
|
-
|
|
203
|
-
const canDismiss = computed(
|
|
204
|
-
() =>
|
|
205
|
-
props.dismissable &&
|
|
206
|
-
step.value !== 'requesting' &&
|
|
207
|
-
step.value !== 'waiting',
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
const initializeRequest = async (request = cachedRequest.value) => {
|
|
211
|
-
cachedRequest.value = request
|
|
212
|
-
error.value = ''
|
|
213
|
-
tx.value = null
|
|
214
|
-
receipt.value = null
|
|
215
|
-
step.value = 'confirm'
|
|
216
|
-
|
|
217
|
-
if (!(await checkChain())) {
|
|
218
|
-
step.value = 'chain'
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
step.value = 'requesting'
|
|
224
|
-
tx.value = await request!()
|
|
225
|
-
step.value = 'waiting'
|
|
226
|
-
const receiptObject = await waitForTransactionReceipt($wagmi as Config, {
|
|
227
|
-
hash: tx.value,
|
|
228
|
-
})
|
|
229
|
-
await delay(props.delayAfter)
|
|
230
|
-
receipt.value = receiptObject
|
|
231
|
-
emit('complete', receiptObject)
|
|
232
|
-
step.value = 'complete'
|
|
233
|
-
} catch (e: unknown) {
|
|
234
|
-
const err = e as { cause?: { code?: number }; shortMessage?: string }
|
|
235
|
-
if (err?.cause?.code === 4001) {
|
|
236
|
-
error.value = 'Transaction rejected by user.'
|
|
237
|
-
step.value = 'error'
|
|
238
|
-
} else {
|
|
239
|
-
error.value = err.shortMessage || 'Error submitting transaction request.'
|
|
240
|
-
step.value = 'error'
|
|
241
|
-
}
|
|
242
|
-
console.log(e)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (props.autoCloseSuccess && step.value === 'complete') {
|
|
246
|
-
await delay(props.delayAutoclose)
|
|
247
|
-
step.value = 'idle'
|
|
248
|
-
await delay(300)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return receipt.value
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const start = () => {
|
|
255
|
-
if (props.skipConfirmation && step.value === 'idle') {
|
|
256
|
-
initializeRequest()
|
|
257
|
-
return
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
step.value = 'confirm'
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const cancel = () => {
|
|
264
|
-
step.value = 'idle'
|
|
265
|
-
error.value = ''
|
|
266
|
-
emit('cancel')
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
defineExpose({
|
|
270
|
-
initializeRequest,
|
|
271
|
-
})
|
|
272
|
-
</script>
|
|
273
|
-
|
|
274
|
-
<style>
|
|
275
|
-
.transaction-flow > section {
|
|
276
|
-
display: grid;
|
|
277
|
-
gap: var(--spacer);
|
|
278
|
-
|
|
279
|
-
.text {
|
|
280
|
-
width: 100%;
|
|
281
|
-
height: min-content;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
p {
|
|
285
|
-
white-space: pre-wrap;
|
|
286
|
-
width: 100%;
|
|
287
|
-
|
|
288
|
-
a {
|
|
289
|
-
text-decoration: underline;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
</style>
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export const useClipboard = () => {
|
|
2
|
-
const copied = ref(false)
|
|
3
|
-
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
4
|
-
|
|
5
|
-
const copy = async (text: string) => {
|
|
6
|
-
try {
|
|
7
|
-
await navigator.clipboard.writeText(text)
|
|
8
|
-
copied.value = true
|
|
9
|
-
|
|
10
|
-
if (timeout) clearTimeout(timeout)
|
|
11
|
-
timeout = setTimeout(() => {
|
|
12
|
-
copied.value = false
|
|
13
|
-
}, 2000)
|
|
14
|
-
|
|
15
|
-
return true
|
|
16
|
-
} catch (error) {
|
|
17
|
-
console.error('Failed to copy to clipboard:', error)
|
|
18
|
-
return false
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
copy,
|
|
24
|
-
copied,
|
|
25
|
-
}
|
|
26
|
-
}
|
package/app/composables/ens.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { getPublicClient } from '@wagmi/core'
|
|
2
|
-
import type { Config } from '@wagmi/vue'
|
|
3
|
-
|
|
4
|
-
type EnsMode = 'indexer' | 'chain'
|
|
5
|
-
|
|
6
|
-
interface UseEnsOptions {
|
|
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 }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function useEnsBase(
|
|
47
|
-
tier: string,
|
|
48
|
-
identifier: MaybeRefOrGetter<string | undefined>,
|
|
49
|
-
chainKeys: string[],
|
|
50
|
-
options: UseEnsOptions = {},
|
|
51
|
-
) {
|
|
52
|
-
const { $wagmi } = useNuxtApp()
|
|
53
|
-
const appConfig = useAppConfig()
|
|
54
|
-
const runtimeConfig = useRuntimeConfig()
|
|
55
|
-
|
|
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)}`)
|
|
59
|
-
|
|
60
|
-
return useAsyncData(
|
|
61
|
-
cacheKey.value,
|
|
62
|
-
async () => {
|
|
63
|
-
const id = toValue(identifier)
|
|
64
|
-
if (!id) return null
|
|
65
|
-
|
|
66
|
-
const strategies: EnsMode[] = mode.value === 'indexer'
|
|
67
|
-
? ['indexer', 'chain']
|
|
68
|
-
: ['chain', 'indexer']
|
|
69
|
-
|
|
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,
|
|
77
|
-
},
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export const useEns = (identifier: MaybeRefOrGetter<string | undefined>, options?: UseEnsOptions) =>
|
|
82
|
-
useEnsBase('resolve', identifier, [], options)
|
|
83
|
-
|
|
84
|
-
export const useEnsWithAvatar = (identifier: MaybeRefOrGetter<string | undefined>, options?: UseEnsOptions) =>
|
|
85
|
-
useEnsBase('avatar', identifier, [...ENS_KEYS_AVATAR], options)
|
|
86
|
-
|
|
87
|
-
export const useEnsProfile = (identifier: MaybeRefOrGetter<string | undefined>, options?: UseEnsOptions) =>
|
|
88
|
-
useEnsBase('profile', identifier, [...ENS_KEYS_PROFILE], options)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
package/app/utils/cache.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { isAddress, type PublicClient, type Address } from 'viem'
|
|
2
|
-
import { normalize } from 'viem/ens'
|
|
3
|
-
|
|
4
|
-
// --- Types ---
|
|
5
|
-
|
|
6
|
-
export interface EnsProfile {
|
|
7
|
-
address: string
|
|
8
|
-
ens: string | null
|
|
9
|
-
data: {
|
|
10
|
-
avatar: string
|
|
11
|
-
header: string
|
|
12
|
-
description: string
|
|
13
|
-
links: {
|
|
14
|
-
url: string
|
|
15
|
-
email: string
|
|
16
|
-
twitter: string
|
|
17
|
-
github: string
|
|
18
|
-
}
|
|
19
|
-
} | null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// --- Text record keys ---
|
|
23
|
-
|
|
24
|
-
const ALL_KEYS = ['avatar', 'header', 'description', 'url', 'email', 'com.twitter', 'com.github'] as const
|
|
25
|
-
|
|
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 ---
|
|
34
|
-
|
|
35
|
-
export async function fetchEnsFromIndexer(
|
|
36
|
-
identifier: string,
|
|
37
|
-
urls: string[],
|
|
38
|
-
): Promise<EnsProfile> {
|
|
39
|
-
let lastError: Error | undefined
|
|
40
|
-
|
|
41
|
-
for (const url of urls) {
|
|
42
|
-
try {
|
|
43
|
-
return await $fetch<EnsProfile>(`${url}/${identifier}`)
|
|
44
|
-
} catch (err) {
|
|
45
|
-
lastError = err as Error
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
throw lastError ?? new Error('No indexer URLs provided')
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function fetchEnsFromChain(
|
|
53
|
-
identifier: string,
|
|
54
|
-
client: PublicClient,
|
|
55
|
-
keys: string[] = [],
|
|
56
|
-
): Promise<EnsProfile> {
|
|
57
|
-
const isAddr = isAddress(identifier)
|
|
58
|
-
|
|
59
|
-
let address: string
|
|
60
|
-
let ens: string | null
|
|
61
|
-
|
|
62
|
-
if (isAddr) {
|
|
63
|
-
address = identifier
|
|
64
|
-
ens = await client.getEnsName({ address: identifier as Address }) ?? null
|
|
65
|
-
} else {
|
|
66
|
-
ens = identifier
|
|
67
|
-
const resolved = await client.getEnsAddress({ name: normalize(identifier) })
|
|
68
|
-
if (!resolved) return { address: '', ens, data: null }
|
|
69
|
-
address = resolved
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!ens || !keys.length) return { address, ens: ens ?? null, data: null }
|
|
73
|
-
|
|
74
|
-
const name = normalize(ens)
|
|
75
|
-
const results = await Promise.all(
|
|
76
|
-
keys.map(key => client.getEnsText({ name, key }).catch(() => null)),
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
return { address, ens, data: toProfileData(keys, results.map(r => r || '')) }
|
|
80
|
-
}
|
|
81
|
-
|
|
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
|
-
}
|