@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.
@@ -1,7 +1,5 @@
1
1
  {
2
2
  "permissions": {
3
- "allow": [
4
- "Bash(pnpm typecheck:*)"
5
- ]
3
+ "allow": ["Bash(pnpm typecheck:*)"]
6
4
  }
7
5
  }
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,3 +1,4 @@
1
1
  <template>
2
2
  <NuxtPage />
3
+ <Toasts />
3
4
  </template>
@@ -18,8 +18,7 @@
18
18
  </div>
19
19
  </template>
20
20
 
21
- <script setup lang="ts">
22
- </script>
21
+ <script setup lang="ts"></script>
23
22
 
24
23
  <style scoped>
25
24
  .playground {
@@ -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 with ENS resolution
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
@@ -38,7 +38,7 @@ Then add the dependency to their `extends` in `nuxt.config`:
38
38
 
39
39
  ```ts
40
40
  defineNuxtConfig({
41
- extends: 'your-layer'
41
+ extends: 'your-layer',
42
42
  })
43
43
  ```
44
44
 
@@ -1,5 +1 @@
1
- export const useBaseURL = () => {
2
- const config = useRuntimeConfig()
3
-
4
- return config.app.baseURL.endsWith('/') ? config.app.baseURL : config.app.baseURL + '/'
5
- }
1
+ export { useBaseURL } from '@1001-digital/components'
@@ -1,42 +1,6 @@
1
- import { useConnection, useSwitchChain } from '@wagmi/vue'
2
-
3
- interface ChainConfig {
4
- id?: number
5
- blockExplorer?: string
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'
@@ -9,14 +9,25 @@ import {
9
9
  type Config,
10
10
  type CreateConnectorFn,
11
11
  } from '@wagmi/vue'
12
- import { coinbaseWallet, injected, metaMask, walletConnect } from '@wagmi/vue/connectors'
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, rpc2?: string, rpc3?: 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
- nuxtApp.vueApp.use(WagmiPlugin, { config: wagmiConfig }).use(VueQueryPlugin, {})
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: {
@@ -1,4 +1 @@
1
- import type { Address } from 'viem'
2
-
3
- export const shortAddress = (address: Address, length: number = 3) =>
4
- address.substring(0, length + 2) + '...' + address.substring(address.length - length)
1
+ export { shortAddress } from '@1001-digital/components'
@@ -1,13 +1 @@
1
- import { defineChain, type Chain } from 'viem'
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'
@@ -1,12 +1 @@
1
- export function formatETH(value: string | number, maxDecimals: number = 3): string {
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.6",
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.26"
13
+ "@1001-digital/layers.base": "^0.0.32"
14
14
  },
15
15
  "peerDependencies": {
16
- "@1001-digital/layers.base": "^0.0.26"
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
- "qrcode": "^1.5.4",
26
- "viem": "~2.45.1"
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,13 +0,0 @@
1
- <template>
2
- <EvmConnectorQR :uri="uri">
3
- <template #instruction>
4
- Scan the code in your MetaMask mobile app
5
- </template>
6
- </EvmConnectorQR>
7
- </template>
8
-
9
- <script setup lang="ts">
10
- defineProps<{
11
- uri: string
12
- }>()
13
- </script>
@@ -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,13 +0,0 @@
1
- <template>
2
- <EvmConnectorQR :uri="uri">
3
- <template #instruction>
4
- Scan the code in your wallet application
5
- </template>
6
- </EvmConnectorQR>
7
- </template>
8
-
9
- <script setup lang="ts">
10
- defineProps<{
11
- uri: string
12
- }>()
13
- </script>
@@ -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
- }
@@ -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))
@@ -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
- }