@1001-digital/layers.evm 0.0.7 → 1.0.0

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 ADDED
@@ -0,0 +1,4 @@
1
+ NUXT_PUBLIC_EVM_WALLET_CONNECT_PROJECT_ID=""
2
+ NUXT_PUBLIC_EVM_CHAINS_MAINNET_RPC1=""
3
+ NUXT_PUBLIC_EVM_CHAINS_MAINNET_RPC2=""
4
+ NUXT_PUBLIC_EVM_CHAINS_MAINNET_RPC3=""
@@ -1,5 +1,12 @@
1
1
  export default defineAppConfig({
2
- myLayer: {
3
- name: 'My amazing Nuxt layer (overwritten)'
4
- }
2
+ evm: {
3
+ title: 'EVM Layer Playground',
4
+ defaultChain: 'mainnet',
5
+ chains: {
6
+ mainnet: {
7
+ id: 1,
8
+ blockExplorer: 'https://etherscan.io',
9
+ },
10
+ },
11
+ },
5
12
  })
package/AGENTS.md CHANGED
@@ -22,7 +22,7 @@ Uses modern wagmi 0.4.x patterns:
22
22
  - `useConnectionEffect` (not deprecated `useAccountEffect`)
23
23
  - `useSwitchConnection` (not deprecated `useSwitchAccount`)
24
24
 
25
- Configured chains: mainnet, sepolia, holesky, localhost
25
+ Configured chains: resolved dynamically from `app.config.ts` via `evm.chains` map (supports mainnet, sepolia, holesky, optimism, arbitrum, base, polygon, localhost out of the box)
26
26
 
27
27
  Connectors: injected, coinbaseWallet, metaMask, walletConnect
28
28
 
@@ -37,7 +37,9 @@ Connectors: injected, coinbaseWallet, metaMask, walletConnect
37
37
 
38
38
  ## Composables
39
39
 
40
- - `useMainChainId()` - Get configured chain ID from runtime config
40
+ - `useChainConfig(key?)` - Get `{ id, blockExplorer }` for a named chain (defaults to `defaultChain`)
41
+ - `useMainChainId()` - Get main chain ID from app config
42
+ - `useBlockExplorer(key?)` - Get block explorer URL for a named chain
41
43
  - `useEnsureChainIdCheck()` - Validate/switch chain before transactions
42
44
  - `useBaseURL()` - Get base URL with trailing slash
43
45
  - `useClipboard()` - Copy text to clipboard with copied state
@@ -46,17 +48,29 @@ Connectors: injected, coinbaseWallet, metaMask, walletConnect
46
48
 
47
49
  - `shortAddress(address, length)` - Truncate address for display
48
50
  - `formatETH(value, maxDecimals)` - Format ETH values
51
+ - `resolveChain(id)` - Resolve chain ID to viem Chain object
49
52
 
50
- ## Environment Variables
53
+ ## Configuration
54
+
55
+ Static chain config lives in `app.config.ts` (safe to commit):
56
+
57
+ ```ts
58
+ evm: {
59
+ title: 'My dApp',
60
+ defaultChain: 'mainnet',
61
+ chains: {
62
+ mainnet: { id: 1, blockExplorer: 'https://etherscan.io' },
63
+ },
64
+ }
65
+ ```
66
+
67
+ Sensitive RPC URLs live in `runtimeConfig.public.evm` (env-driven):
51
68
 
52
69
  ```bash
53
- NUXT_PUBLIC_TITLE="App Name"
54
- NUXT_PUBLIC_CHAIN_ID=1
55
- NUXT_PUBLIC_BLOCK_EXPLORER="https://etherscan.io"
56
- NUXT_PUBLIC_RPC1=""
57
- NUXT_PUBLIC_RPC2=""
58
- NUXT_PUBLIC_RPC3=""
59
- NUXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=""
70
+ NUXT_PUBLIC_EVM_WALLET_CONNECT_PROJECT_ID=""
71
+ NUXT_PUBLIC_EVM_CHAINS_MAINNET_RPC1=""
72
+ NUXT_PUBLIC_EVM_CHAINS_MAINNET_RPC2=""
73
+ NUXT_PUBLIC_EVM_CHAINS_MAINNET_RPC3=""
60
74
  ```
61
75
 
62
76
  ## Key directories
package/app/app.config.ts CHANGED
@@ -1,14 +1,30 @@
1
1
  export default defineAppConfig({
2
2
  evm: {
3
- name: 'Hello from Nuxt layer'
4
- }
3
+ title: 'EVM Layer',
4
+ defaultChain: 'mainnet',
5
+ chains: {
6
+ mainnet: {
7
+ id: 1,
8
+ blockExplorer: 'https://etherscan.io',
9
+ },
10
+ },
11
+ },
5
12
  })
6
13
 
14
+ interface EvmChainConfig {
15
+ id?: number
16
+ blockExplorer?: string
17
+ }
18
+
7
19
  declare module '@nuxt/schema' {
8
20
  interface AppConfigInput {
9
21
  evm?: {
10
- /** Project name */
11
- name?: string
22
+ /** App title */
23
+ title?: string
24
+ /** Key into `chains` that serves as the app's primary chain */
25
+ defaultChain?: string
26
+ /** Named chain definitions */
27
+ chains?: Record<string, EvmChainConfig>
12
28
  }
13
29
  }
14
30
  }
@@ -1,7 +1,12 @@
1
1
  <template>
2
- <slot :start="start" name="start"></slot>
3
-
4
- <Dialog v-model:open="open" :x-close="false" class="transaction-flow">
2
+ <slot :start="start" :step="step" :open="open" name="start"></slot>
3
+
4
+ <Dialog
5
+ v-model:open="open"
6
+ :x-close="canDismiss"
7
+ :click-outside="canDismiss"
8
+ class="transaction-flow"
9
+ >
5
10
  <slot name="before" />
6
11
 
7
12
  <h1 v-if="text.title[step]">{{ text.title[step] }}</h1>
@@ -13,7 +18,12 @@
13
18
 
14
19
  <slot :name="step" :cancel="cancel"></slot>
15
20
 
16
- <Button v-if="step === 'waiting'" :to="txLink" target="_blank" class="block-explorer">
21
+ <Button
22
+ v-if="step === 'waiting'"
23
+ :to="txLink"
24
+ target="_blank"
25
+ class="block-explorer"
26
+ >
17
27
  <Icon type="loader" class="spin" />
18
28
  <span>View on Block Explorer</span>
19
29
  </Button>
@@ -24,85 +34,95 @@
24
34
 
25
35
  <Actions v-if="step === 'confirm' || step === 'error'">
26
36
  <Button @click="cancel" class="secondary">Cancel</Button>
27
- <Button @click="() => initializeRequest()">{{ text.action[step] || 'Execute' }}</Button>
37
+ <Button @click="() => initializeRequest()">{{
38
+ text.action[step] || "Execute"
39
+ }}</Button>
28
40
  </Actions>
29
41
  </Dialog>
30
42
  </template>
31
43
 
32
44
  <script setup lang="ts">
33
- import { waitForTransactionReceipt, watchChainId } from '@wagmi/core'
34
- import type { Config } from '@wagmi/vue'
35
- import type { TransactionReceipt, Hash } from 'viem'
45
+ import { waitForTransactionReceipt, watchChainId } from "@wagmi/core";
46
+ import type { Config } from "@wagmi/vue";
47
+ import type { TransactionReceipt, Hash } from "viem";
36
48
 
37
49
  interface TextConfig {
38
- title: Record<string, string>
39
- lead: Record<string, string>
40
- action: Record<string, string>
50
+ title: Record<string, string>;
51
+ lead: Record<string, string>;
52
+ action: Record<string, string>;
41
53
  }
42
54
 
43
- const checkChain = useEnsureChainIdCheck()
44
-
45
- const { $wagmi } = useNuxtApp()
46
- const config = useRuntimeConfig()
47
-
48
- const props = withDefaults(defineProps<{
49
- text?: TextConfig
50
- request?: () => Promise<Hash>
51
- delayAfter?: number
52
- delayAutoclose?: number
53
- skipConfirmation?: boolean
54
- autoCloseSuccess?: boolean
55
- }>(), {
56
- text: () => ({
57
- title: {
58
- confirm: 'Confirm Transaction',
59
- },
60
- lead: {
61
- confirm: 'Please review and confirm this transaction.',
62
- },
63
- action: {
64
- confirm: 'Execute',
65
- },
66
- }),
67
- delayAfter: 2000,
68
- delayAutoclose: 2000,
69
- skipConfirmation: false,
70
- autoCloseSuccess: false,
71
- })
55
+ const checkChain = useEnsureChainIdCheck();
56
+
57
+ const { $wagmi } = useNuxtApp();
58
+ const blockExplorer = useBlockExplorer();
59
+
60
+ const props = withDefaults(
61
+ defineProps<{
62
+ text?: TextConfig;
63
+ request?: () => Promise<Hash>;
64
+ delayAfter?: number;
65
+ delayAutoclose?: number;
66
+ skipConfirmation?: boolean;
67
+ autoCloseSuccess?: boolean;
68
+ dismissable?: boolean;
69
+ }>(),
70
+ {
71
+ text: () => ({
72
+ title: {
73
+ confirm: "Confirm Transaction",
74
+ },
75
+ lead: {
76
+ confirm: "Please review and confirm this transaction.",
77
+ },
78
+ action: {
79
+ confirm: "Execute",
80
+ },
81
+ }),
82
+ delayAfter: 2000,
83
+ delayAutoclose: 2000,
84
+ skipConfirmation: false,
85
+ autoCloseSuccess: true,
86
+ dismissable: true,
87
+ },
88
+ );
72
89
 
73
90
  const emit = defineEmits<{
74
- complete: [receipt: TransactionReceipt]
75
- cancel: []
76
- }>()
91
+ complete: [receipt: TransactionReceipt];
92
+ cancel: [];
93
+ }>();
77
94
 
78
- const open = ref(false)
95
+ const open = ref(false);
79
96
 
80
- const switchChain = ref(false)
97
+ const switchChain = ref(false);
81
98
  watchChainId($wagmi as Config, {
82
99
  async onChange() {
83
- if (!switchChain.value) return
100
+ if (!switchChain.value) return;
84
101
 
85
102
  if (await checkChain()) {
86
- switchChain.value = false
87
- initializeRequest()
103
+ switchChain.value = false;
104
+ initializeRequest();
88
105
  } else {
89
- switchChain.value = true
106
+ switchChain.value = true;
90
107
  }
91
108
  },
92
- })
109
+ });
93
110
 
94
- const cachedRequest = ref(props.request)
95
- watch(() => props.request, () => {
96
- cachedRequest.value = props.request
97
- })
111
+ const cachedRequest = ref(props.request);
112
+ watch(
113
+ () => props.request,
114
+ () => {
115
+ cachedRequest.value = props.request;
116
+ },
117
+ );
98
118
 
99
- const requesting = ref(false)
100
- const waiting = ref(false)
101
- const complete = ref(false)
102
- const error = ref('')
103
- const tx = ref<Hash | null>(null)
104
- const receipt = ref<TransactionReceipt | null>(null)
105
- const txLink = computed(() => `${config.public.blockExplorer}/tx/${tx.value}`)
119
+ const requesting = ref(false);
120
+ const waiting = ref(false);
121
+ const complete = ref(false);
122
+ const error = ref("");
123
+ const tx = ref<Hash | null>(null);
124
+ const receipt = ref<TransactionReceipt | null>(null);
125
+ const txLink = computed(() => `${blockExplorer}/tx/${tx.value}`);
106
126
 
107
127
  const step = computed(() => {
108
128
  if (
@@ -112,96 +132,100 @@ const step = computed(() => {
112
132
  !waiting.value &&
113
133
  !complete.value
114
134
  ) {
115
- return 'confirm'
135
+ return "confirm";
116
136
  }
117
137
 
118
138
  if (switchChain.value) {
119
- return 'chain'
139
+ return "chain";
120
140
  }
121
141
 
122
142
  if (requesting.value) {
123
- return 'requesting'
143
+ return "requesting";
124
144
  }
125
145
 
126
146
  if (waiting.value) {
127
- return 'waiting'
147
+ return "waiting";
128
148
  }
129
149
 
130
150
  if (complete.value) {
131
- return 'complete'
151
+ return "complete";
132
152
  }
133
153
 
134
- return 'error'
135
- })
154
+ return "error";
155
+ });
156
+
157
+ const canDismiss = computed(
158
+ () => props.dismissable && step.value !== "requesting",
159
+ );
136
160
 
137
161
  const initializeRequest = async (request = cachedRequest.value) => {
138
- cachedRequest.value = request
139
- complete.value = false
140
- open.value = true
141
- error.value = ''
142
- tx.value = null
143
- receipt.value = null
162
+ cachedRequest.value = request;
163
+ complete.value = false;
164
+ open.value = true;
165
+ error.value = "";
166
+ tx.value = null;
167
+ receipt.value = null;
144
168
 
145
169
  if (!(await checkChain())) {
146
- switchChain.value = true
147
- return
170
+ switchChain.value = true;
171
+ return;
148
172
  } else {
149
- switchChain.value = false
173
+ switchChain.value = false;
150
174
  }
151
175
 
152
- if (requesting.value) return
176
+ if (requesting.value) return;
153
177
 
154
178
  try {
155
- requesting.value = true
156
- tx.value = await request!()
157
- requesting.value = false
158
- waiting.value = true
179
+ requesting.value = true;
180
+ tx.value = await request!();
181
+ requesting.value = false;
182
+ waiting.value = true;
159
183
  const [receiptObject] = await Promise.all([
160
184
  waitForTransactionReceipt($wagmi as Config, { hash: tx.value }),
161
- ])
162
- await delay(props.delayAfter)
163
- receipt.value = receiptObject
164
- emit('complete', receiptObject)
165
- complete.value = true
185
+ ]);
186
+ await delay(props.delayAfter);
187
+ receipt.value = receiptObject;
188
+ emit("complete", receiptObject);
189
+ complete.value = true;
166
190
  } catch (e: unknown) {
167
- const err = e as { cause?: { code?: number }; shortMessage?: string }
191
+ const err = e as { cause?: { code?: number }; shortMessage?: string };
168
192
  if (err?.cause?.code === 4001) {
169
- open.value = false
193
+ open.value = false;
170
194
  } else {
171
- error.value = err.shortMessage || 'Error submitting transaction request.'
195
+ error.value = err.shortMessage || "Error submitting transaction request.";
172
196
  }
173
- console.log(e)
197
+ console.log(e);
174
198
  }
175
199
 
176
- requesting.value = false
177
- waiting.value = false
200
+ requesting.value = false;
201
+ waiting.value = false;
178
202
 
179
- if (props.autoCloseSuccess && step.value === 'complete') {
180
- await delay(props.delayAutoclose)
181
- open.value = false
182
- await delay(300)
203
+ if (props.autoCloseSuccess && step.value === "complete") {
204
+ await delay(props.delayAutoclose);
205
+ open.value = false;
206
+ await delay(300);
183
207
  }
184
208
 
185
- return receipt.value
186
- }
209
+ return receipt.value;
210
+ };
187
211
 
188
212
  const start = () => {
189
213
  if (props.skipConfirmation && !open.value) {
190
- initializeRequest()
214
+ initializeRequest();
191
215
  }
192
216
 
193
- open.value = true
194
- }
217
+ open.value = true;
218
+ };
195
219
 
196
220
  const cancel = () => {
197
- open.value = false
221
+ open.value = false;
198
222
 
199
- emit('cancel')
200
- }
223
+ emit("cancel");
224
+ };
201
225
 
202
226
  defineExpose({
203
227
  initializeRequest,
204
- })
228
+ });
205
229
  </script>
206
230
 
207
231
  <style>
@@ -1,11 +1,28 @@
1
1
  import { useConnection, useSwitchChain } from '@wagmi/vue'
2
2
 
3
- export const useMainChainId = () => {
4
- const config = useRuntimeConfig()
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]
5
15
 
6
- return config.public.chainId as 1 | 11155111 | 17000 | 1337 | 31337
16
+ return {
17
+ id: chain?.id ?? 1,
18
+ blockExplorer: chain?.blockExplorer ?? 'https://etherscan.io',
19
+ }
7
20
  }
8
21
 
22
+ export const useMainChainId = () => useChainConfig().id
23
+
24
+ export const useBlockExplorer = (key?: string) => useChainConfig(key).blockExplorer
25
+
9
26
  export const useEnsureChainIdCheck = () => {
10
27
  const chainId = useMainChainId()
11
28
  const { switchChain } = useSwitchChain()
@@ -9,14 +9,38 @@ import {
9
9
  type Config,
10
10
  type CreateConnectorFn,
11
11
  } from '@wagmi/vue'
12
- import { mainnet, sepolia, holesky, localhost } from '@wagmi/vue/chains'
13
12
  import { coinbaseWallet, injected, metaMask, walletConnect } from '@wagmi/vue/connectors'
14
- import type { CustomTransport, Transport } from 'viem'
13
+ import type { Chain, Transport } from 'viem'
15
14
 
16
15
  export default defineNuxtPlugin((nuxtApp) => {
17
- const title = nuxtApp.$config.public.title || 'EVM Layer'
18
- const mainChainId = nuxtApp.$config.public.chainId
16
+ const appConfig = useAppConfig()
17
+ const runtimeConfig = nuxtApp.$config.public.evm as {
18
+ walletConnectProjectId: string
19
+ chains: Record<string, { rpc1?: string, rpc2?: string, rpc3?: string }>
20
+ }
21
+
22
+ const title = appConfig.evm?.title || 'EVM Layer'
23
+ const chainEntries = appConfig.evm?.chains || {}
24
+
25
+ // Build chains and transports from config
26
+ const chains: [Chain, ...Chain[]] = [] as unknown as [Chain, ...Chain[]]
27
+ const transports: Record<number, Transport> = {}
19
28
 
29
+ for (const [key, entry] of Object.entries(chainEntries)) {
30
+ const chain = resolveChain(entry.id!)
31
+ chains.push(chain)
32
+
33
+ const rpcs = runtimeConfig.chains?.[key]
34
+ const transportList = []
35
+ if (rpcs?.rpc1) transportList.push(http(rpcs.rpc1))
36
+ if (rpcs?.rpc2) transportList.push(http(rpcs.rpc2))
37
+ if (rpcs?.rpc3) transportList.push(http(rpcs.rpc3))
38
+ transportList.push(http())
39
+
40
+ transports[chain.id] = fallback(transportList)
41
+ }
42
+
43
+ // Connectors
20
44
  const connectors: CreateConnectorFn[] = [
21
45
  injected(),
22
46
  coinbaseWallet({
@@ -33,28 +57,16 @@ export default defineNuxtPlugin((nuxtApp) => {
33
57
  }),
34
58
  ]
35
59
 
36
- if (import.meta.client && nuxtApp.$config.public.walletConnectProjectId)
60
+ if (import.meta.client && runtimeConfig.walletConnectProjectId)
37
61
  connectors.push(
38
62
  walletConnect({
39
- projectId: nuxtApp.$config.public.walletConnectProjectId,
63
+ projectId: runtimeConfig.walletConnectProjectId,
40
64
  showQrModal: false,
41
65
  }),
42
66
  )
43
67
 
44
- const transportDefinitions: CustomTransport | Transport[] = []
45
-
46
- if (nuxtApp.$config.public.rpc1)
47
- transportDefinitions.push(http(nuxtApp.$config.public.rpc1 as string))
48
- if (nuxtApp.$config.public.rpc2)
49
- transportDefinitions.push(http(nuxtApp.$config.public.rpc2 as string))
50
- if (nuxtApp.$config.public.rpc3)
51
- transportDefinitions.push(http(nuxtApp.$config.public.rpc3 as string))
52
- transportDefinitions.push(http())
53
-
54
- const transports = fallback(transportDefinitions)
55
-
56
68
  const wagmiConfig: Config = createConfig({
57
- chains: [mainnet, sepolia, holesky, localhost],
69
+ chains,
58
70
  batch: {
59
71
  multicall: true,
60
72
  },
@@ -63,12 +75,7 @@ export default defineNuxtPlugin((nuxtApp) => {
63
75
  storage: cookieStorage,
64
76
  }),
65
77
  ssr: true,
66
- transports: {
67
- [mainnet.id]: mainChainId == 1 ? transports : http(),
68
- [sepolia.id]: transports,
69
- [holesky.id]: transports,
70
- [localhost.id]: transports,
71
- },
78
+ transports,
72
79
  })
73
80
 
74
81
  nuxtApp.vueApp.use(WagmiPlugin, { config: wagmiConfig }).use(VueQueryPlugin, {})
@@ -0,0 +1,13 @@
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
+ })
package/nuxt.config.ts CHANGED
@@ -8,13 +8,12 @@ export default defineNuxtConfig({
8
8
 
9
9
  runtimeConfig: {
10
10
  public: {
11
- title: 'EVM Layer',
12
- blockExplorer: 'https://etherscan.io',
13
- chainId: 1,
14
- rpc1: '',
15
- rpc2: '',
16
- rpc3: '',
17
- walletConnectProjectId: '',
11
+ evm: {
12
+ walletConnectProjectId: '',
13
+ chains: {
14
+ mainnet: { rpc1: '', rpc2: '', rpc3: '' },
15
+ },
16
+ },
18
17
  },
19
18
  },
20
19
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@1001-digital/layers.evm",
3
3
  "type": "module",
4
- "version": "0.0.7",
4
+ "version": "1.0.0",
5
5
  "main": "./nuxt.config.ts",
6
6
  "devDependencies": {
7
7
  "@nuxt/eslint": "latest",
@@ -10,15 +10,16 @@
10
10
  "nuxt": "^4.3.0",
11
11
  "typescript": "^5.9.3",
12
12
  "vue": "latest",
13
- "@1001-digital/layers.base": "^0.0.14"
13
+ "@1001-digital/layers.base": "^0.0.17"
14
14
  },
15
15
  "peerDependencies": {
16
- "@1001-digital/layers.base": "^0.0.14"
16
+ "@1001-digital/layers.base": "^0.0.17"
17
17
  },
18
18
  "dependencies": {
19
19
  "@types/qrcode": "^1.5.6",
20
20
  "@metamask/sdk": "~0.34.0",
21
21
  "@tanstack/vue-query": "^5.92.9",
22
+ "@wagmi/core": "^3.3.2",
22
23
  "@wagmi/vue": "^0.4.15",
23
24
  "@walletconnect/ethereum-provider": "~2.23.4",
24
25
  "qrcode": "^1.5.4",
@@ -1,7 +0,0 @@
1
- NUXT_PUBLIC_TITLE="EVM Layer Playground"
2
- NUXT_PUBLIC_CHAIN_ID=1
3
- NUXT_PUBLIC_BLOCK_EXPLORER="https://etherscan.io"
4
- NUXT_PUBLIC_RPC1=""
5
- NUXT_PUBLIC_RPC2=""
6
- NUXT_PUBLIC_RPC3=""
7
- NUXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=""