@1001-digital/layers.evm 0.0.1
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/.editorconfig +12 -0
- package/.nuxtrc +1 -0
- package/.playground/.env.example +7 -0
- package/.playground/app.config.ts +5 -0
- package/.playground/app.vue +3 -0
- package/.playground/components/SignedIn.client.vue +106 -0
- package/.playground/nuxt.config.ts +12 -0
- package/.playground/pages/index.vue +32 -0
- package/AGENTS.md +72 -0
- package/README.md +73 -0
- package/app/components/EvmAccount.client.vue +28 -0
- package/app/components/EvmConnect.client.vue +176 -0
- package/app/components/EvmConnectorQR.client.vue +108 -0
- package/app/components/EvmMetaMaskQR.client.vue +13 -0
- package/app/components/EvmTransactionFlow.vue +237 -0
- package/app/components/EvmWalletConnectQR.client.vue +13 -0
- package/app/composables/base.ts +5 -0
- package/app/composables/chainId.ts +25 -0
- package/app/composables/clipboard.ts +26 -0
- package/app/composables/helpers.ts +1 -0
- package/app/plugins/wagmi.ts +81 -0
- package/app/utils/addresses.ts +4 -0
- package/app/utils/format-eth.ts +12 -0
- package/app.config.ts +14 -0
- package/eslint.config.js +3 -0
- package/nuxt.config.ts +41 -0
- package/package.json +32 -0
- package/public/icons/wallets/coinbase.svg +4 -0
- package/public/icons/wallets/metamask.svg +1 -0
- package/public/icons/wallets/phantom.svg +4 -0
- package/public/icons/wallets/rabby.svg +24 -0
- package/public/icons/wallets/rainbow.svg +59 -0
- package/public/icons/wallets/walletconnect.svg +1 -0
- package/tsconfig.json +3 -0
package/.editorconfig
ADDED
package/.nuxtrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
typescript.includeWorkspace = true
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Card v-if="isConnected">
|
|
3
|
+
<h2>Disconnect?</h2>
|
|
4
|
+
<Button @click="disconnect">Disconnect</Button>
|
|
5
|
+
</Card>
|
|
6
|
+
|
|
7
|
+
<Card v-if="isConnected">
|
|
8
|
+
<h2>Account Info</h2>
|
|
9
|
+
<p>Address: {{ address }}</p>
|
|
10
|
+
<p>Chain ID: {{ chainId }}</p>
|
|
11
|
+
</Card>
|
|
12
|
+
|
|
13
|
+
<Card v-if="isConnected">
|
|
14
|
+
<h2>Transaction Flow Example</h2>
|
|
15
|
+
<p>Send 0 ETH to your own address</p>
|
|
16
|
+
|
|
17
|
+
<EvmTransactionFlow :request="sendTransaction" :text="{
|
|
18
|
+
title: {
|
|
19
|
+
confirm: 'Send Transaction',
|
|
20
|
+
requesting: 'Requesting...',
|
|
21
|
+
waiting: 'Waiting for confirmation...',
|
|
22
|
+
complete: 'Transaction Complete!',
|
|
23
|
+
error: 'Transaction Error',
|
|
24
|
+
},
|
|
25
|
+
lead: {
|
|
26
|
+
confirm: 'This will send 0 ETH to your address as a test transaction.',
|
|
27
|
+
requesting: 'Please confirm the transaction in your wallet.',
|
|
28
|
+
waiting: 'Your transaction is being processed...',
|
|
29
|
+
complete: 'Your transaction has been confirmed on-chain.',
|
|
30
|
+
error: 'An error occurred while processing your transaction.',
|
|
31
|
+
},
|
|
32
|
+
action: {
|
|
33
|
+
confirm: 'Send Transaction',
|
|
34
|
+
error: 'Try Again',
|
|
35
|
+
},
|
|
36
|
+
}" @complete="onTransactionComplete" @cancel="onTransactionCancel">
|
|
37
|
+
<template #start="{ start }">
|
|
38
|
+
<Actions>
|
|
39
|
+
<Button @click="start">Start Transaction</Button>
|
|
40
|
+
</Actions>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<template #confirm>
|
|
44
|
+
<div class="tx-details">
|
|
45
|
+
<p><strong>To:</strong> {{ address }}</p>
|
|
46
|
+
<p><strong>Amount:</strong> 0 ETH</p>
|
|
47
|
+
<p><strong>Chain:</strong> {{ chainId }}</p>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<template #complete>
|
|
52
|
+
<p class="success">Transaction confirmed successfully!</p>
|
|
53
|
+
</template>
|
|
54
|
+
</EvmTransactionFlow>
|
|
55
|
+
</Card>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
import { useConnection, useDisconnect } from '@wagmi/vue'
|
|
60
|
+
import { sendTransaction as sendTx } from '@wagmi/core'
|
|
61
|
+
import { parseEther } from 'viem'
|
|
62
|
+
import type { Config } from '@wagmi/vue'
|
|
63
|
+
import type { TransactionReceipt } from 'viem'
|
|
64
|
+
|
|
65
|
+
const { $wagmi } = useNuxtApp()
|
|
66
|
+
const { address, isConnected, chainId } = useConnection()
|
|
67
|
+
const { disconnect } = useDisconnect()
|
|
68
|
+
|
|
69
|
+
const sendTransaction = async () => {
|
|
70
|
+
const hash = await sendTx($wagmi as Config, {
|
|
71
|
+
to: address.value!,
|
|
72
|
+
value: parseEther('0'),
|
|
73
|
+
})
|
|
74
|
+
return hash
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const onTransactionComplete = (receipt: TransactionReceipt) => {
|
|
78
|
+
console.log('Transaction complete:', receipt)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const onTransactionCancel = () => {
|
|
82
|
+
console.log('Transaction cancelled')
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<style scoped>
|
|
87
|
+
.tx-details {
|
|
88
|
+
padding: var(--size-4);
|
|
89
|
+
background: var(--gray-z-1);
|
|
90
|
+
border-radius: var(--radius);
|
|
91
|
+
display: grid;
|
|
92
|
+
gap: var(--size-3);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.tx-details p {
|
|
96
|
+
margin: 0;
|
|
97
|
+
font-family: var(--font-mono);
|
|
98
|
+
font-size: var(--font-sm);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.success {
|
|
102
|
+
color: var(--success);
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
text-align: center;
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
|
|
3
|
+
export default defineNuxtConfig({
|
|
4
|
+
extends: ['..'],
|
|
5
|
+
modules: ['@nuxt/eslint'],
|
|
6
|
+
eslint: {
|
|
7
|
+
config: {
|
|
8
|
+
// Use the generated ESLint config for lint root project as well
|
|
9
|
+
rootDir: fileURLToPath(new URL('..', import.meta.url))
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="playground">
|
|
3
|
+
<h1>EVM Layer Playground</h1>
|
|
4
|
+
|
|
5
|
+
<Card>
|
|
6
|
+
<h2>Wallet Connection</h2>
|
|
7
|
+
<EvmConnect>
|
|
8
|
+
<template #connected="{ address }">
|
|
9
|
+
<p>
|
|
10
|
+
Connected:
|
|
11
|
+
<EvmAccount :address="address" />
|
|
12
|
+
</p>
|
|
13
|
+
</template>
|
|
14
|
+
</EvmConnect>
|
|
15
|
+
</Card>
|
|
16
|
+
|
|
17
|
+
<SignedIn />
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<style scoped>
|
|
25
|
+
.playground {
|
|
26
|
+
max-width: 50rem;
|
|
27
|
+
margin: 0 auto;
|
|
28
|
+
padding: var(--spacer);
|
|
29
|
+
display: grid;
|
|
30
|
+
gap: var(--spacer);
|
|
31
|
+
}
|
|
32
|
+
</style>
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Nuxt layer for building dAPPs (Ethereum-powered applications). Extends `@1001-digital/layers.base`.
|
|
4
|
+
|
|
5
|
+
## Setup commands
|
|
6
|
+
|
|
7
|
+
- Install deps: `pnpm install`
|
|
8
|
+
- Start dev server: `pnpm dev`
|
|
9
|
+
- Prepare types: `pnpm dev:prepare`
|
|
10
|
+
|
|
11
|
+
## Dependencies
|
|
12
|
+
|
|
13
|
+
- `@wagmi/vue` (0.4.x) - Wallet connection, contract reads/writes, account state
|
|
14
|
+
- `viem` - Type-safe Ethereum utilities, ABI encoding, transaction simulation
|
|
15
|
+
- `@tanstack/vue-query` - Caching and synchronization of blockchain data
|
|
16
|
+
- `qrcode` - QR code generation for wallet connect URIs
|
|
17
|
+
|
|
18
|
+
## Wagmi Configuration
|
|
19
|
+
|
|
20
|
+
Uses modern wagmi 0.4.x patterns:
|
|
21
|
+
- `useConnection` (not deprecated `useAccount`)
|
|
22
|
+
- `useConnectionEffect` (not deprecated `useAccountEffect`)
|
|
23
|
+
- `useSwitchConnection` (not deprecated `useSwitchAccount`)
|
|
24
|
+
|
|
25
|
+
Configured chains: mainnet, sepolia, holesky, localhost
|
|
26
|
+
|
|
27
|
+
Connectors: injected, coinbaseWallet, metaMask, walletConnect
|
|
28
|
+
|
|
29
|
+
## Components
|
|
30
|
+
|
|
31
|
+
- `EvmConnect.client.vue` - Wallet connection button with modal
|
|
32
|
+
- `EvmAccount.client.vue` - Address display with ENS resolution
|
|
33
|
+
- `EvmTransactionFlow.vue` - Guided transaction execution flow
|
|
34
|
+
- `EvmConnectorQR.client.vue` - Base QR code renderer
|
|
35
|
+
- `EvmWalletConnectQR.client.vue` - WalletConnect QR wrapper
|
|
36
|
+
- `EvmMetaMaskQR.client.vue` - MetaMask QR wrapper
|
|
37
|
+
|
|
38
|
+
## Composables
|
|
39
|
+
|
|
40
|
+
- `useMainChainId()` - Get configured chain ID from runtime config
|
|
41
|
+
- `useEnsureChainIdCheck()` - Validate/switch chain before transactions
|
|
42
|
+
- `useBaseURL()` - Get base URL with trailing slash
|
|
43
|
+
- `useClipboard()` - Copy text to clipboard with copied state
|
|
44
|
+
|
|
45
|
+
## Utilities
|
|
46
|
+
|
|
47
|
+
- `shortAddress(address, length)` - Truncate address for display
|
|
48
|
+
- `formatETH(value, maxDecimals)` - Format ETH values
|
|
49
|
+
|
|
50
|
+
## Environment Variables
|
|
51
|
+
|
|
52
|
+
```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=""
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Key directories
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
app/
|
|
66
|
+
├── components/ # Vue components (Evm* prefixed)
|
|
67
|
+
├── composables/ # Composables (chainId, helpers)
|
|
68
|
+
├── plugins/ # Wagmi plugin configuration
|
|
69
|
+
└── utils/ # Utility functions
|
|
70
|
+
public/
|
|
71
|
+
└── icons/wallets/ # Wallet connector icons
|
|
72
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Nuxt Layer Starter
|
|
2
|
+
|
|
3
|
+
Create Nuxt extendable layer with this GitHub template.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Make sure to install the dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Working on your layer
|
|
14
|
+
|
|
15
|
+
Your layer is at the root of this repository, it is exactly like a regular Nuxt project, except you can publish it on NPM.
|
|
16
|
+
|
|
17
|
+
The `.playground` directory should help you on trying your layer during development.
|
|
18
|
+
|
|
19
|
+
Running `pnpm dev` will prepare and boot `.playground` directory, which imports your layer itself.
|
|
20
|
+
|
|
21
|
+
## Distributing your layer
|
|
22
|
+
|
|
23
|
+
Your Nuxt layer is shaped exactly the same as any other Nuxt project, except you can publish it on NPM.
|
|
24
|
+
|
|
25
|
+
To do so, you only have to check if `files` in `package.json` are valid, then run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm publish --access public
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Once done, your users will only have to run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install --save your-layer
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then add the dependency to their `extends` in `nuxt.config`:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
defineNuxtConfig({
|
|
41
|
+
extends: 'your-layer'
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Development Server
|
|
46
|
+
|
|
47
|
+
Start the development server on http://localhost:3000
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm dev
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Production
|
|
54
|
+
|
|
55
|
+
Build the application for production:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pnpm build
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or statically generate it with:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pnpm generate
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Locally preview production build:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm preview
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Checkout the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
|
@@ -0,0 +1,28 @@
|
|
|
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, useEnsName } from '@wagmi/vue'
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
address?: Address
|
|
13
|
+
}>()
|
|
14
|
+
const address = computed(() => props.address)
|
|
15
|
+
|
|
16
|
+
const { address: currentAddress } = useConnection()
|
|
17
|
+
|
|
18
|
+
const isCurrent = computed<boolean>(
|
|
19
|
+
() => currentAddress.value?.toLowerCase() === address.value?.toLowerCase(),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const { data: ens } = useEnsName({
|
|
23
|
+
address,
|
|
24
|
+
chainId: 1,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const display = computed<string>(() => ens.value || shortAddress(address.value!))
|
|
28
|
+
</script>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Button v-if="showConnect" @click="chooseModalOpen = true" :class="className">
|
|
3
|
+
<slot>Connect Wallet</slot>
|
|
4
|
+
</Button>
|
|
5
|
+
<slot v-else name="connected" :address="address">
|
|
6
|
+
<EvmAccount :address="address" />
|
|
7
|
+
</slot>
|
|
8
|
+
|
|
9
|
+
<Teleport to="body">
|
|
10
|
+
<Dialog v-if="showConnect" title="Connect Wallet" v-model:open="chooseModalOpen" @closed="onModalClosed">
|
|
11
|
+
<Alert v-if="errorMessage" type="error">
|
|
12
|
+
{{ errorMessage }}
|
|
13
|
+
</Alert>
|
|
14
|
+
<EvmWalletConnectQR v-if="walletConnectUri" :uri="walletConnectUri" />
|
|
15
|
+
<EvmMetaMaskQR v-else-if="metaMaskUri" :uri="metaMaskUri" />
|
|
16
|
+
<template v-else-if="isConnecting">
|
|
17
|
+
<Loading txt="Waiting for wallet confirmation..." spinner />
|
|
18
|
+
</template>
|
|
19
|
+
<div v-else class="wallet-options">
|
|
20
|
+
<Button v-for="connector in shownConnectors" :key="connector.uid" @click="() => login(connector)"
|
|
21
|
+
class="choose-connector">
|
|
22
|
+
<img v-if="ICONS[connector.name]" :src="connector.icon || `${base}icons/wallets/${ICONS[connector.name]}`"
|
|
23
|
+
:alt="connector.name" />
|
|
24
|
+
<span>{{ connector.name }}</span>
|
|
25
|
+
</Button>
|
|
26
|
+
<Button to="https://ethereum.org/wallets/" target="_blank" class="link muted small">
|
|
27
|
+
<Icon type="help" />
|
|
28
|
+
<span>New to wallets?</span>
|
|
29
|
+
</Button>
|
|
30
|
+
</div>
|
|
31
|
+
</Dialog>
|
|
32
|
+
</Teleport>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup lang="ts">
|
|
36
|
+
import type { Connector } from '@wagmi/vue'
|
|
37
|
+
import { useConnection, useConnect, useChainId } from '@wagmi/vue'
|
|
38
|
+
|
|
39
|
+
const ICONS: Record<string, string> = {
|
|
40
|
+
'Coinbase Wallet': 'coinbase.svg',
|
|
41
|
+
MetaMask: 'metamask.svg',
|
|
42
|
+
Phantom: 'phantom.svg',
|
|
43
|
+
'Rabby Wallet': 'rabby.svg',
|
|
44
|
+
Rainbow: 'rainbow.svg',
|
|
45
|
+
WalletConnect: 'walletconnect.svg',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const PRIORITY: Record<string, number> = {
|
|
49
|
+
'WalletConnect': 20,
|
|
50
|
+
'Coinbase Wallet': 10,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const props = defineProps<{
|
|
54
|
+
className?: string
|
|
55
|
+
}>()
|
|
56
|
+
const emit = defineEmits<{
|
|
57
|
+
connected: [{ address: `0x${string}` | undefined }]
|
|
58
|
+
disconnected: []
|
|
59
|
+
}>()
|
|
60
|
+
const base = useBaseURL()
|
|
61
|
+
|
|
62
|
+
const chainId = useChainId()
|
|
63
|
+
const { connectors, connectAsync } = useConnect()
|
|
64
|
+
const { address, isConnected } = useConnection()
|
|
65
|
+
|
|
66
|
+
const showConnect = computed(() => !isConnected.value)
|
|
67
|
+
const shownConnectors = computed(() => {
|
|
68
|
+
const unique = Array.from(
|
|
69
|
+
new Map(connectors?.map((connector) => [connector.name, connector])).values(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const filtered = unique.length > 1 ? unique.filter((c) => c.id !== 'injected') : unique
|
|
73
|
+
|
|
74
|
+
return filtered.sort((a, b) => {
|
|
75
|
+
const priorityA = PRIORITY[a.name] ?? 5
|
|
76
|
+
const priorityB = PRIORITY[b.name] ?? 5
|
|
77
|
+
return priorityA - priorityB
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const chooseModalOpen = ref(false)
|
|
82
|
+
const errorMessage = ref('')
|
|
83
|
+
const isConnecting = ref(false)
|
|
84
|
+
const walletConnectUri = ref('')
|
|
85
|
+
const metaMaskUri = ref('')
|
|
86
|
+
|
|
87
|
+
const login = async (connector: Connector) => {
|
|
88
|
+
errorMessage.value = ''
|
|
89
|
+
isConnecting.value = true
|
|
90
|
+
walletConnectUri.value = ''
|
|
91
|
+
metaMaskUri.value = ''
|
|
92
|
+
|
|
93
|
+
const handleMessage = (event: { type: string; data?: string }) => {
|
|
94
|
+
if (event.type === 'display_uri' && event.data) {
|
|
95
|
+
if (connector.id === 'walletConnect') {
|
|
96
|
+
walletConnectUri.value = event.data
|
|
97
|
+
} else if (connector.id === 'metaMaskSDK') {
|
|
98
|
+
metaMaskUri.value = event.data
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (connector.id === 'walletConnect' || connector.id === 'metaMaskSDK') {
|
|
104
|
+
connector.emitter.on('message', handleMessage)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await connectAsync({ connector, chainId: chainId.value })
|
|
109
|
+
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
chooseModalOpen.value = false
|
|
112
|
+
isConnecting.value = false
|
|
113
|
+
walletConnectUri.value = ''
|
|
114
|
+
metaMaskUri.value = ''
|
|
115
|
+
}, 100)
|
|
116
|
+
} catch (error: unknown) {
|
|
117
|
+
isConnecting.value = false
|
|
118
|
+
walletConnectUri.value = ''
|
|
119
|
+
metaMaskUri.value = ''
|
|
120
|
+
|
|
121
|
+
const errorMsg = error instanceof Error ? error.message : ''
|
|
122
|
+
if (errorMsg.includes('User rejected') || errorMsg.includes('rejected') || errorMsg.includes('denied')) {
|
|
123
|
+
errorMessage.value = 'Connection cancelled. Please try again.'
|
|
124
|
+
} else {
|
|
125
|
+
errorMessage.value = 'Failed to connect. Please try again.'
|
|
126
|
+
}
|
|
127
|
+
console.error('Wallet connection error:', error)
|
|
128
|
+
} finally {
|
|
129
|
+
if (connector.id === 'walletConnect' || connector.id === 'metaMaskSDK') {
|
|
130
|
+
connector.emitter.off('message', handleMessage)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const onModalClosed = () => {
|
|
136
|
+
errorMessage.value = ''
|
|
137
|
+
isConnecting.value = false
|
|
138
|
+
walletConnectUri.value = ''
|
|
139
|
+
metaMaskUri.value = ''
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const check = () =>
|
|
143
|
+
isConnected.value ? emit('connected', { address: address.value }) : emit('disconnected')
|
|
144
|
+
watch(isConnected, () => check())
|
|
145
|
+
onMounted(() => check())
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<style scoped>
|
|
149
|
+
.wallet-options {
|
|
150
|
+
display: grid;
|
|
151
|
+
gap: var(--spacer);
|
|
152
|
+
|
|
153
|
+
button.choose-connector {
|
|
154
|
+
width: 100%;
|
|
155
|
+
inline-size: auto;
|
|
156
|
+
justify-content: flex-start;
|
|
157
|
+
|
|
158
|
+
img {
|
|
159
|
+
margin: -1rem 0 -1rem -0.6rem;
|
|
160
|
+
width: var(--size-5);
|
|
161
|
+
height: var(--size-5);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
span {
|
|
165
|
+
border-left: var(--border);
|
|
166
|
+
padding-left: var(--spacer-sm);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.link.muted {
|
|
173
|
+
justify-self: center;
|
|
174
|
+
font-size: var(--font-xs);
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -0,0 +1,108 @@
|
|
|
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 ? 'checkmark' : '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-sm, 0.5rem);
|
|
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>
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<slot :start="start" name="start"></slot>
|
|
3
|
+
|
|
4
|
+
<Dialog v-model:open="open" :x-close="false" class="transaction-flow">
|
|
5
|
+
<slot name="before" />
|
|
6
|
+
|
|
7
|
+
<h1 v-if="text.title[step]">{{ text.title[step] }}</h1>
|
|
8
|
+
|
|
9
|
+
<div class="text">
|
|
10
|
+
<p v-if="text.lead[step]">{{ text.lead[step] }}</p>
|
|
11
|
+
<p v-if="error">{{ error }}</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<slot :name="step" :cancel="cancel"></slot>
|
|
15
|
+
|
|
16
|
+
<Button v-if="step === 'waiting'" :to="txLink" target="_blank" class="block-explorer">
|
|
17
|
+
<Icon type="loader" class="spin" />
|
|
18
|
+
<span>View on Block Explorer</span>
|
|
19
|
+
</Button>
|
|
20
|
+
|
|
21
|
+
<Actions v-if="step === 'chain'">
|
|
22
|
+
<Button @click="cancel" class="secondary">Cancel</Button>
|
|
23
|
+
</Actions>
|
|
24
|
+
|
|
25
|
+
<Actions v-if="step === 'confirm' || step === 'error'">
|
|
26
|
+
<Button @click="cancel" class="secondary">Cancel</Button>
|
|
27
|
+
<Button @click="() => initializeRequest()">{{ text.action[step] || 'Execute' }}</Button>
|
|
28
|
+
</Actions>
|
|
29
|
+
</Dialog>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<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'
|
|
36
|
+
|
|
37
|
+
interface TextConfig {
|
|
38
|
+
title: Record<string, string>
|
|
39
|
+
lead: Record<string, string>
|
|
40
|
+
action: Record<string, string>
|
|
41
|
+
}
|
|
42
|
+
|
|
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
|
+
})
|
|
72
|
+
|
|
73
|
+
const emit = defineEmits<{
|
|
74
|
+
complete: [receipt: TransactionReceipt]
|
|
75
|
+
cancel: []
|
|
76
|
+
}>()
|
|
77
|
+
|
|
78
|
+
const open = ref(false)
|
|
79
|
+
|
|
80
|
+
const switchChain = ref(false)
|
|
81
|
+
watchChainId($wagmi as Config, {
|
|
82
|
+
async onChange() {
|
|
83
|
+
if (!switchChain.value) return
|
|
84
|
+
|
|
85
|
+
if (await checkChain()) {
|
|
86
|
+
switchChain.value = false
|
|
87
|
+
initializeRequest()
|
|
88
|
+
} else {
|
|
89
|
+
switchChain.value = true
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const cachedRequest = ref(props.request)
|
|
95
|
+
watch(() => props.request, () => {
|
|
96
|
+
cachedRequest.value = props.request
|
|
97
|
+
})
|
|
98
|
+
|
|
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}`)
|
|
106
|
+
|
|
107
|
+
const step = computed(() => {
|
|
108
|
+
if (
|
|
109
|
+
open.value &&
|
|
110
|
+
!requesting.value &&
|
|
111
|
+
!switchChain.value &&
|
|
112
|
+
!waiting.value &&
|
|
113
|
+
!complete.value
|
|
114
|
+
) {
|
|
115
|
+
return 'confirm'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (switchChain.value) {
|
|
119
|
+
return 'chain'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (requesting.value) {
|
|
123
|
+
return 'requesting'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (waiting.value) {
|
|
127
|
+
return 'waiting'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (complete.value) {
|
|
131
|
+
return 'complete'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return 'error'
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
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
|
|
144
|
+
|
|
145
|
+
if (!(await checkChain())) {
|
|
146
|
+
switchChain.value = true
|
|
147
|
+
return
|
|
148
|
+
} else {
|
|
149
|
+
switchChain.value = false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (requesting.value) return
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
requesting.value = true
|
|
156
|
+
tx.value = await request!()
|
|
157
|
+
requesting.value = false
|
|
158
|
+
waiting.value = true
|
|
159
|
+
const [receiptObject] = await Promise.all([
|
|
160
|
+
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
|
|
166
|
+
} catch (e: unknown) {
|
|
167
|
+
const err = e as { cause?: { code?: number }; shortMessage?: string }
|
|
168
|
+
if (err?.cause?.code === 4001) {
|
|
169
|
+
open.value = false
|
|
170
|
+
} else {
|
|
171
|
+
error.value = err.shortMessage || 'Error submitting transaction request.'
|
|
172
|
+
}
|
|
173
|
+
console.log(e)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
requesting.value = false
|
|
177
|
+
waiting.value = false
|
|
178
|
+
|
|
179
|
+
if (props.autoCloseSuccess && step.value === 'complete') {
|
|
180
|
+
await delay(props.delayAutoclose)
|
|
181
|
+
open.value = false
|
|
182
|
+
await delay(300)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return receipt.value
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const start = () => {
|
|
189
|
+
if (props.skipConfirmation && !open.value) {
|
|
190
|
+
initializeRequest()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
open.value = true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const cancel = () => {
|
|
197
|
+
open.value = false
|
|
198
|
+
|
|
199
|
+
emit('cancel')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
defineExpose({
|
|
203
|
+
initializeRequest,
|
|
204
|
+
})
|
|
205
|
+
</script>
|
|
206
|
+
|
|
207
|
+
<style>
|
|
208
|
+
.transaction-flow {
|
|
209
|
+
display: grid;
|
|
210
|
+
gap: var(--spacer);
|
|
211
|
+
|
|
212
|
+
.spinner {
|
|
213
|
+
width: var(--size-7);
|
|
214
|
+
height: var(--size-7);
|
|
215
|
+
margin: calc(-1 * var(--size-4)) 0 var(--size-3);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.text {
|
|
219
|
+
width: 100%;
|
|
220
|
+
height: min-content;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
h1 {
|
|
224
|
+
font-size: var(--font-lg);
|
|
225
|
+
margin-bottom: var(--size-4);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
p {
|
|
229
|
+
white-space: pre-wrap;
|
|
230
|
+
width: 100%;
|
|
231
|
+
|
|
232
|
+
a {
|
|
233
|
+
text-decoration: underline;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useConnection, useSwitchChain } from '@wagmi/vue'
|
|
2
|
+
|
|
3
|
+
export const useMainChainId = () => {
|
|
4
|
+
const config = useRuntimeConfig()
|
|
5
|
+
|
|
6
|
+
return config.public.chainId as 1 | 11155111 | 17000 | 1337 | 31337
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useEnsureChainIdCheck = () => {
|
|
10
|
+
const chainId = useMainChainId()
|
|
11
|
+
const { switchChain } = useSwitchChain()
|
|
12
|
+
const { chainId: currentChainId } = useConnection()
|
|
13
|
+
|
|
14
|
+
return async () => {
|
|
15
|
+
if (chainId !== currentChainId.value) {
|
|
16
|
+
switchChain({ chainId })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (chainId === currentChainId.value) {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { VueQueryPlugin } from '@tanstack/vue-query'
|
|
2
|
+
import {
|
|
3
|
+
http,
|
|
4
|
+
cookieStorage,
|
|
5
|
+
createConfig,
|
|
6
|
+
createStorage,
|
|
7
|
+
WagmiPlugin,
|
|
8
|
+
fallback,
|
|
9
|
+
type Config,
|
|
10
|
+
type CreateConnectorFn,
|
|
11
|
+
} from '@wagmi/vue'
|
|
12
|
+
import { mainnet, sepolia, holesky, localhost } from '@wagmi/vue/chains'
|
|
13
|
+
import { coinbaseWallet, injected, metaMask, walletConnect } from '@wagmi/vue/connectors'
|
|
14
|
+
import type { CustomTransport, Transport } from 'viem'
|
|
15
|
+
|
|
16
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
17
|
+
const title = nuxtApp.$config.public.title || 'EVM Layer'
|
|
18
|
+
const mainChainId = nuxtApp.$config.public.chainId
|
|
19
|
+
|
|
20
|
+
const connectors: CreateConnectorFn[] = [
|
|
21
|
+
injected(),
|
|
22
|
+
coinbaseWallet({
|
|
23
|
+
appName: title,
|
|
24
|
+
appLogoUrl: '',
|
|
25
|
+
}),
|
|
26
|
+
metaMask({
|
|
27
|
+
headless: true,
|
|
28
|
+
dappMetadata: {
|
|
29
|
+
name: title,
|
|
30
|
+
iconUrl: '',
|
|
31
|
+
url: '',
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
if (import.meta.client && nuxtApp.$config.public.walletConnectProjectId)
|
|
37
|
+
connectors.push(
|
|
38
|
+
walletConnect({
|
|
39
|
+
projectId: nuxtApp.$config.public.walletConnectProjectId,
|
|
40
|
+
showQrModal: false,
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
|
|
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
|
+
const wagmiConfig: Config = createConfig({
|
|
57
|
+
chains: [mainnet, sepolia, holesky, localhost],
|
|
58
|
+
batch: {
|
|
59
|
+
multicall: true,
|
|
60
|
+
},
|
|
61
|
+
connectors,
|
|
62
|
+
storage: createStorage({
|
|
63
|
+
storage: cookieStorage,
|
|
64
|
+
}),
|
|
65
|
+
ssr: true,
|
|
66
|
+
transports: {
|
|
67
|
+
[mainnet.id]: mainChainId == 1 ? transports : http(),
|
|
68
|
+
[sepolia.id]: transports,
|
|
69
|
+
[holesky.id]: transports,
|
|
70
|
+
[localhost.id]: transports,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
nuxtApp.vueApp.use(WagmiPlugin, { config: wagmiConfig }).use(VueQueryPlugin, {})
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
provide: {
|
|
78
|
+
wagmi: wagmiConfig,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
}
|
package/app.config.ts
ADDED
package/eslint.config.js
ADDED
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
2
|
+
export default defineNuxtConfig({
|
|
3
|
+
extends: ['@1001-digital/layers.base'],
|
|
4
|
+
|
|
5
|
+
modules: ['@wagmi/vue/nuxt'],
|
|
6
|
+
|
|
7
|
+
ssr: process.env.NUXT_SSR !== 'false',
|
|
8
|
+
|
|
9
|
+
runtimeConfig: {
|
|
10
|
+
public: {
|
|
11
|
+
title: 'EVM Layer',
|
|
12
|
+
blockExplorer: 'https://etherscan.io',
|
|
13
|
+
chainId: 1,
|
|
14
|
+
rpc1: '',
|
|
15
|
+
rpc2: '',
|
|
16
|
+
rpc3: '',
|
|
17
|
+
walletConnectProjectId: '',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
vite: {
|
|
22
|
+
optimizeDeps: {
|
|
23
|
+
include: [
|
|
24
|
+
'@1001-digital/layers.evm > @metamask/sdk',
|
|
25
|
+
'@1001-digital/layers.evm > eventemitter3',
|
|
26
|
+
'@1001-digital/layers.evm > qrcode',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
nitro: {
|
|
32
|
+
preset: 'node-cluster',
|
|
33
|
+
esbuild: {
|
|
34
|
+
options: {
|
|
35
|
+
target: 'esnext',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
compatibilityDate: '2024-11-01',
|
|
41
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@1001-digital/layers.evm",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"main": "./nuxt.config.ts",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@nuxt/eslint": "latest",
|
|
8
|
+
"@types/node": "^24.10.4",
|
|
9
|
+
"@types/qrcode": "^1.5.6",
|
|
10
|
+
"eslint": "^9.39.2",
|
|
11
|
+
"nuxt": "^4.2.2",
|
|
12
|
+
"typescript": "^5.9.3",
|
|
13
|
+
"vue": "latest"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@metamask/sdk": "~0.33.1",
|
|
17
|
+
"@tanstack/vue-query": "^5.92.1",
|
|
18
|
+
"@wagmi/vue": "^0.4.3",
|
|
19
|
+
"@walletconnect/ethereum-provider": "~2.21.1",
|
|
20
|
+
"qrcode": "^1.5.4",
|
|
21
|
+
"viem": "~2.42.1",
|
|
22
|
+
"@1001-digital/layers.base": "^0.0.2"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "nuxi dev .playground",
|
|
26
|
+
"dev:prepare": "nuxt prepare .playground",
|
|
27
|
+
"build": "nuxt build .playground",
|
|
28
|
+
"generate": "nuxt generate .playground",
|
|
29
|
+
"preview": "nuxt preview .playground",
|
|
30
|
+
"lint": "eslint ."
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="1024" height="1024" fill="#0052FF"/>
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M152 512C152 710.823 313.177 872 512 872C710.823 872 872 710.823 872 512C872 313.177 710.823 152 512 152C313.177 152 152 313.177 152 512ZM420 396C406.745 396 396 406.745 396 420V604C396 617.255 406.745 628 420 628H604C617.255 628 628 617.255 628 604V420C628 406.745 617.255 396 604 396H420Z" fill="white"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" height="33" viewBox="0 0 35 33" width="35" xmlns="http://www.w3.org/2000/svg"><g stroke-linecap="round" stroke-linejoin="round" stroke-width=".25"><path d="m32.9582 1-13.1341 9.7183 2.4424-5.72731z" fill="#e17726" stroke="#e17726"/><g fill="#e27625" stroke="#e27625"><path d="m2.66296 1 13.01714 9.809-2.3254-5.81802z"/><path d="m28.2295 23.5335-3.4947 5.3386 7.4829 2.0603 2.1436-7.2823z"/><path d="m1.27281 23.6501 2.13055 7.2823 7.46994-2.0603-3.48166-5.3386z"/><path d="m10.4706 14.5149-2.0786 3.1358 7.405.3369-.2469-7.969z"/><path d="m25.1505 14.5149-5.1575-4.58704-.1688 8.05974 7.4049-.3369z"/><path d="m10.8733 28.8721 4.4819-2.1639-3.8583-3.0062z"/><path d="m20.2659 26.7082 4.4689 2.1639-.6105-5.1701z"/></g><path d="m24.7348 28.8721-4.469-2.1639.3638 2.9025-.039 1.231z" fill="#d5bfb2" stroke="#d5bfb2"/><path d="m10.8732 28.8721 4.1572 1.9696-.026-1.231.3508-2.9025z" fill="#d5bfb2" stroke="#d5bfb2"/><path d="m15.1084 21.7842-3.7155-1.0884 2.6243-1.2051z" fill="#233447" stroke="#233447"/><path d="m20.5126 21.7842 1.0913-2.2935 2.6372 1.2051z" fill="#233447" stroke="#233447"/><path d="m10.8733 28.8721.6495-5.3386-4.13117.1167z" fill="#cc6228" stroke="#cc6228"/><path d="m24.0982 23.5335.6366 5.3386 3.4946-5.2219z" fill="#cc6228" stroke="#cc6228"/><path d="m27.2291 17.6507-7.405.3369.6885 3.7966 1.0913-2.2935 2.6372 1.2051z" fill="#cc6228" stroke="#cc6228"/><path d="m11.3929 20.6958 2.6242-1.2051 1.0913 2.2935.6885-3.7966-7.40495-.3369z" fill="#cc6228" stroke="#cc6228"/><path d="m8.392 17.6507 3.1049 6.0513-.1039-3.0062z" fill="#e27525" stroke="#e27525"/><path d="m24.2412 20.6958-.1169 3.0062 3.1049-6.0513z" fill="#e27525" stroke="#e27525"/><path d="m15.797 17.9876-.6886 3.7967.8704 4.4833.1949-5.9087z" fill="#e27525" stroke="#e27525"/><path d="m19.8242 17.9876-.3638 2.3584.1819 5.9216.8704-4.4833z" fill="#e27525" stroke="#e27525"/><path d="m20.5127 21.7842-.8704 4.4834.6236.4406 3.8584-3.0062.1169-3.0062z" fill="#f5841f" stroke="#f5841f"/><path d="m11.3929 20.6958.104 3.0062 3.8583 3.0062.6236-.4406-.8704-4.4834z" fill="#f5841f" stroke="#f5841f"/><path d="m20.5906 30.8417.039-1.231-.3378-.2851h-4.9626l-.3248.2851.026 1.231-4.1572-1.9696 1.4551 1.1921 2.9489 2.0344h5.0536l2.962-2.0344 1.442-1.1921z" fill="#c0ac9d" stroke="#c0ac9d"/><path d="m20.2659 26.7082-.6236-.4406h-3.6635l-.6236.4406-.3508 2.9025.3248-.2851h4.9626l.3378.2851z" fill="#161616" stroke="#161616"/><path d="m33.5168 11.3532 1.1043-5.36447-1.6629-4.98873-12.6923 9.3944 4.8846 4.1205 6.8983 2.0085 1.52-1.7752-.6626-.4795 1.0523-.9588-.8054-.622 1.0523-.8034z" fill="#763e1a" stroke="#763e1a"/><path d="m1 5.98873 1.11724 5.36447-.71451.5313 1.06527.8034-.80545.622 1.05228.9588-.66255.4795 1.51997 1.7752 6.89835-2.0085 4.8846-4.1205-12.69233-9.3944z" fill="#763e1a" stroke="#763e1a"/><path d="m32.0489 16.5234-6.8983-2.0085 2.0786 3.1358-3.1049 6.0513 4.1052-.0519h6.1318z" fill="#f5841f" stroke="#f5841f"/><path d="m10.4705 14.5149-6.89828 2.0085-2.29944 7.1267h6.11883l4.10519.0519-3.10487-6.0513z" fill="#f5841f" stroke="#f5841f"/><path d="m19.8241 17.9876.4417-7.5932 2.0007-5.4034h-8.9119l2.0006 5.4034.4417 7.5932.1689 2.3842.013 5.8958h3.6635l.013-5.8958z" fill="#f5841f" stroke="#f5841f"/></g></svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="128" height="128" fill="#AB9FF2"/>
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.6416 82.1477C50.8744 89.4525 42.8862 98.6966 32.2568 98.6966C27.232 98.6966 22.4004 96.628 22.4004 87.6424C22.4004 64.7584 53.6445 29.3335 82.6339 29.3335C99.1257 29.3335 105.697 40.7755 105.697 53.7689C105.697 70.4471 94.8739 89.5171 84.1156 89.5171C80.7013 89.5171 79.0264 87.6424 79.0264 84.6688C79.0264 83.8931 79.1552 83.0527 79.4129 82.1477C75.7409 88.4182 68.6546 94.2361 62.0192 94.2361C57.1877 94.2361 54.7397 91.1979 54.7397 86.9314C54.7397 85.3799 55.0618 83.7638 55.6416 82.1477ZM80.6133 53.3182C80.6133 57.1044 78.3795 58.9975 75.8806 58.9975C73.3438 58.9975 71.1479 57.1044 71.1479 53.3182C71.1479 49.532 73.3438 47.6389 75.8806 47.6389C78.3795 47.6389 80.6133 49.532 80.6133 53.3182ZM94.8102 53.3184C94.8102 57.1046 92.5763 58.9977 90.0775 58.9977C87.5407 58.9977 85.3447 57.1046 85.3447 53.3184C85.3447 49.5323 87.5407 47.6392 90.0775 47.6392C92.5763 47.6392 94.8102 49.5323 94.8102 53.3184Z" fill="#FFFDF8"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M438.47 279.097C452.952 246.637 381.359 155.948 312.964 118.165C269.853 88.895 224.93 92.9162 215.832 105.768C195.865 133.972 281.948 157.871 339.518 185.759C327.143 191.152 315.481 200.83 308.623 213.207C287.16 189.697 240.052 169.451 184.777 185.759C147.528 196.749 116.571 222.658 104.606 261.791C101.699 260.495 98.4799 259.774 95.0934 259.774C82.1436 259.774 71.6456 270.308 71.6456 283.301C71.6456 296.295 82.1436 306.828 95.0934 306.828C97.4937 306.828 104.999 305.213 104.999 305.213L224.93 306.085C176.967 382.43 139.063 393.59 139.063 406.817C139.063 420.043 175.331 416.459 188.948 411.529C254.138 387.928 324.155 314.373 336.17 293.199C386.625 299.515 429.028 300.262 438.47 279.097Z" fill="url(#paint0_linear_1758_656)"/>
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M339.513 185.763C339.516 185.764 339.519 185.766 339.522 185.767C342.191 184.712 341.759 180.758 341.026 177.652C339.342 170.515 310.284 141.724 282.997 128.829C245.815 111.257 218.435 112.163 214.39 120.262C221.964 135.837 257.077 150.461 293.748 165.733C309.394 172.249 325.323 178.883 339.519 185.76C339.517 185.761 339.515 185.762 339.513 185.763Z" fill="url(#paint1_linear_1758_656)"/>
|
|
4
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M292.329 342.523C284.809 339.64 276.315 336.994 266.658 334.594C276.955 316.108 279.115 288.74 269.391 271.437C255.743 247.153 238.612 234.228 198.802 234.228C176.907 234.228 117.955 241.628 116.909 291.006C116.799 296.187 116.906 300.935 117.28 305.301L224.93 306.084C210.417 329.185 196.825 346.318 184.926 359.345C199.213 363.019 211.003 366.103 221.828 368.934C232.098 371.62 241.499 374.079 251.339 376.598C266.182 365.748 280.135 353.917 292.329 342.523Z" fill="url(#paint2_linear_1758_656)"/>
|
|
5
|
+
<path d="M103.169 300.228C107.567 337.737 128.813 352.437 172.227 356.788C215.641 361.138 240.544 358.22 273.698 361.246C301.389 363.774 326.113 377.932 335.285 373.04C343.539 368.636 338.921 352.728 327.876 342.521C313.558 329.291 293.742 320.093 258.875 316.828C265.824 297.739 263.877 270.973 253.085 256.411C237.481 235.355 208.68 225.836 172.227 229.995C134.143 234.34 97.6504 253.153 103.169 300.228Z" fill="url(#paint3_linear_1758_656)"/>
|
|
6
|
+
<defs>
|
|
7
|
+
<linearGradient id="paint0_linear_1758_656" x1="180.439" y1="250.352" x2="435.479" y2="322.433" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop stop-color="#8697FF"/>
|
|
9
|
+
<stop offset="1" stop-color="#ABB7FF"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<linearGradient id="paint1_linear_1758_656" x1="392.428" y1="245.489" x2="207.876" y2="61.1077" gradientUnits="userSpaceOnUse">
|
|
12
|
+
<stop stop-color="#8697FF"/>
|
|
13
|
+
<stop offset="1" stop-color="#5156D8" stop-opacity="0"/>
|
|
14
|
+
</linearGradient>
|
|
15
|
+
<linearGradient id="paint2_linear_1758_656" x1="297.446" y1="348.967" x2="120.465" y2="247.558" gradientUnits="userSpaceOnUse">
|
|
16
|
+
<stop stop-color="#465EED"/>
|
|
17
|
+
<stop offset="1" stop-color="#8697FF" stop-opacity="0"/>
|
|
18
|
+
</linearGradient>
|
|
19
|
+
<linearGradient id="paint3_linear_1758_656" x1="195.658" y1="248.443" x2="315.581" y2="400.306" gradientUnits="userSpaceOnUse">
|
|
20
|
+
<stop stop-color="#8898FF"/>
|
|
21
|
+
<stop offset="0.983895" stop-color="#6277F1"/>
|
|
22
|
+
</linearGradient>
|
|
23
|
+
</defs>
|
|
24
|
+
</svg>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g clip-path="url(#clip0_8_458)">
|
|
3
|
+
<rect width="120" height="120" fill="url(#paint0_linear_8_458)"/>
|
|
4
|
+
<path d="M20 38H26C56.9279 38 82 63.0721 82 94V100H94C97.3137 100 100 97.3137 100 94C100 53.1309 66.8691 20 26 20C22.6863 20 20 22.6863 20 26V38Z" fill="url(#paint1_radial_8_458)"/>
|
|
5
|
+
<path d="M84 94H100C100 97.3137 97.3137 100 94 100H84V94Z" fill="url(#paint2_linear_8_458)"/>
|
|
6
|
+
<path d="M26 20L26 36H20L20 26C20 22.6863 22.6863 20 26 20Z" fill="url(#paint3_linear_8_458)"/>
|
|
7
|
+
<path d="M20 36H26C58.0325 36 84 61.9675 84 94V100H66V94C66 71.9086 48.0914 54 26 54H20V36Z" fill="url(#paint4_radial_8_458)"/>
|
|
8
|
+
<path d="M68 94H84V100H68V94Z" fill="url(#paint5_linear_8_458)"/>
|
|
9
|
+
<path d="M20 52L20 36L26 36L26 52H20Z" fill="url(#paint6_linear_8_458)"/>
|
|
10
|
+
<path d="M20 62C20 65.3137 22.6863 68 26 68C40.3594 68 52 79.6406 52 94C52 97.3137 54.6863 100 58 100H68V94C68 70.804 49.196 52 26 52H20V62Z" fill="url(#paint7_radial_8_458)"/>
|
|
11
|
+
<path d="M52 94H68V100H58C54.6863 100 52 97.3137 52 94Z" fill="url(#paint8_radial_8_458)"/>
|
|
12
|
+
<path d="M26 68C22.6863 68 20 65.3137 20 62L20 52L26 52L26 68Z" fill="url(#paint9_radial_8_458)"/>
|
|
13
|
+
</g>
|
|
14
|
+
<defs>
|
|
15
|
+
<linearGradient id="paint0_linear_8_458" x1="60" y1="0" x2="60" y2="120" gradientUnits="userSpaceOnUse">
|
|
16
|
+
<stop stop-color="#174299"/>
|
|
17
|
+
<stop offset="1" stop-color="#001E59"/>
|
|
18
|
+
</linearGradient>
|
|
19
|
+
<radialGradient id="paint1_radial_8_458" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(26 94) rotate(-90) scale(74)">
|
|
20
|
+
<stop offset="0.770277" stop-color="#FF4000"/>
|
|
21
|
+
<stop offset="1" stop-color="#8754C9"/>
|
|
22
|
+
</radialGradient>
|
|
23
|
+
<linearGradient id="paint2_linear_8_458" x1="83" y1="97" x2="100" y2="97" gradientUnits="userSpaceOnUse">
|
|
24
|
+
<stop stop-color="#FF4000"/>
|
|
25
|
+
<stop offset="1" stop-color="#8754C9"/>
|
|
26
|
+
</linearGradient>
|
|
27
|
+
<linearGradient id="paint3_linear_8_458" x1="23" y1="20" x2="23" y2="37" gradientUnits="userSpaceOnUse">
|
|
28
|
+
<stop stop-color="#8754C9"/>
|
|
29
|
+
<stop offset="1" stop-color="#FF4000"/>
|
|
30
|
+
</linearGradient>
|
|
31
|
+
<radialGradient id="paint4_radial_8_458" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(26 94) rotate(-90) scale(58)">
|
|
32
|
+
<stop offset="0.723929" stop-color="#FFF700"/>
|
|
33
|
+
<stop offset="1" stop-color="#FF9901"/>
|
|
34
|
+
</radialGradient>
|
|
35
|
+
<linearGradient id="paint5_linear_8_458" x1="68" y1="97" x2="84" y2="97" gradientUnits="userSpaceOnUse">
|
|
36
|
+
<stop stop-color="#FFF700"/>
|
|
37
|
+
<stop offset="1" stop-color="#FF9901"/>
|
|
38
|
+
</linearGradient>
|
|
39
|
+
<linearGradient id="paint6_linear_8_458" x1="23" y1="52" x2="23" y2="36" gradientUnits="userSpaceOnUse">
|
|
40
|
+
<stop stop-color="#FFF700"/>
|
|
41
|
+
<stop offset="1" stop-color="#FF9901"/>
|
|
42
|
+
</linearGradient>
|
|
43
|
+
<radialGradient id="paint7_radial_8_458" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(26 94) rotate(-90) scale(42)">
|
|
44
|
+
<stop offset="0.59513" stop-color="#00AAFF"/>
|
|
45
|
+
<stop offset="1" stop-color="#01DA40"/>
|
|
46
|
+
</radialGradient>
|
|
47
|
+
<radialGradient id="paint8_radial_8_458" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(51 97) scale(17 45.3333)">
|
|
48
|
+
<stop stop-color="#00AAFF"/>
|
|
49
|
+
<stop offset="1" stop-color="#01DA40"/>
|
|
50
|
+
</radialGradient>
|
|
51
|
+
<radialGradient id="paint9_radial_8_458" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23 69) rotate(-90) scale(17 322.37)">
|
|
52
|
+
<stop stop-color="#00AAFF"/>
|
|
53
|
+
<stop offset="1" stop-color="#01DA40"/>
|
|
54
|
+
</radialGradient>
|
|
55
|
+
<clipPath id="clip0_8_458">
|
|
56
|
+
<rect width="120" height="120" fill="white"/>
|
|
57
|
+
</clipPath>
|
|
58
|
+
</defs>
|
|
59
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" height="400" viewBox="0 0 400 400" width="400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m0 0h400v400h-400z"/></clipPath><g clip-path="url(#a)"><circle cx="200" cy="200" fill="#3396ff" r="199.5" stroke="#66b1ff"/><path d="m122.519 148.965c42.791-41.729 112.171-41.729 154.962 0l5.15 5.022c2.14 2.086 2.14 5.469 0 7.555l-17.617 17.18c-1.07 1.043-2.804 1.043-3.874 0l-7.087-6.911c-29.853-29.111-78.253-29.111-108.106 0l-7.59 7.401c-1.07 1.043-2.804 1.043-3.874 0l-17.617-17.18c-2.14-2.086-2.14-5.469 0-7.555zm191.397 35.529 15.679 15.29c2.14 2.086 2.14 5.469 0 7.555l-70.7 68.944c-2.139 2.087-5.608 2.087-7.748 0l-50.178-48.931c-.535-.522-1.402-.522-1.937 0l-50.178 48.931c-2.139 2.087-5.608 2.087-7.748 0l-70.7015-68.945c-2.1396-2.086-2.1396-5.469 0-7.555l15.6795-15.29c2.1396-2.086 5.6085-2.086 7.7481 0l50.1789 48.932c.535.522 1.402.522 1.937 0l50.177-48.932c2.139-2.087 5.608-2.087 7.748 0l50.179 48.932c.535.522 1.402.522 1.937 0l50.179-48.931c2.139-2.087 5.608-2.087 7.748 0z" fill="#fff"/></g></svg>
|
package/tsconfig.json
ADDED