@1001-digital/components 1.1.10 → 1.2.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/package.json +1 -1
- package/src/evm/components/EvmConnect.vue +102 -153
- package/src/evm/components/EvmConnectDialog.vue +75 -0
- package/src/evm/components/EvmConnectionStatus.vue +13 -0
- package/src/evm/components/EvmSiwe.vue +190 -0
- package/src/evm/components/EvmSiweDialog.vue +93 -0
- package/src/evm/composables/siwe.ts +89 -0
- package/src/evm/index.ts +8 -0
- package/src/evm/utils/siwe.ts +70 -0
package/package.json
CHANGED
|
@@ -1,131 +1,102 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
v-if="
|
|
4
|
-
@
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
<EvmInAppWalletSetup
|
|
3
|
+
v-if="showInAppSetup"
|
|
4
|
+
@connected="onInAppConnected"
|
|
5
|
+
@back="showInAppSetup = false"
|
|
6
|
+
/>
|
|
7
|
+
<template v-else-if="errorMessage">
|
|
8
|
+
<Alert type="error">
|
|
9
|
+
{{ errorMessage }}
|
|
10
|
+
</Alert>
|
|
11
|
+
<Button
|
|
12
|
+
class="link muted small"
|
|
13
|
+
@click="resetConnection"
|
|
14
|
+
>
|
|
15
|
+
<Icon type="chevron-left" />
|
|
16
|
+
<span>Back</span>
|
|
17
|
+
</Button>
|
|
18
|
+
</template>
|
|
19
|
+
<EvmMetaMaskQR
|
|
20
|
+
v-else-if="metaMaskUri"
|
|
21
|
+
:uri="metaMaskUri"
|
|
22
|
+
@back="resetConnection"
|
|
23
|
+
/>
|
|
24
|
+
<EvmWalletConnectWallets
|
|
25
|
+
v-else-if="walletConnectUri"
|
|
26
|
+
:uri="walletConnectUri"
|
|
27
|
+
@back="resetConnection"
|
|
28
|
+
/>
|
|
29
|
+
<template v-else-if="isConnecting">
|
|
30
|
+
<Loading
|
|
31
|
+
:txt="`Waiting for ${connectingWallet} confirmation...`"
|
|
32
|
+
spinner
|
|
33
|
+
stacked
|
|
34
|
+
/>
|
|
35
|
+
</template>
|
|
36
|
+
<div
|
|
10
37
|
v-else
|
|
11
|
-
|
|
12
|
-
:address="address"
|
|
38
|
+
class="wallet-options"
|
|
13
39
|
>
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<template v-else-if="errorMessage">
|
|
29
|
-
<Alert type="error">
|
|
30
|
-
{{ errorMessage }}
|
|
31
|
-
</Alert>
|
|
32
|
-
<Button
|
|
33
|
-
class="link muted small"
|
|
34
|
-
@click="resetConnection"
|
|
40
|
+
<Button
|
|
41
|
+
v-for="connector in shownConnectors"
|
|
42
|
+
:key="connector.uid"
|
|
43
|
+
@click="() => login(connector)"
|
|
44
|
+
class="block choose-connector"
|
|
45
|
+
>
|
|
46
|
+
<img
|
|
47
|
+
v-if="ICONS[connector.name] || connector.icon"
|
|
48
|
+
:src="ICONS[connector.name] || connector.icon"
|
|
49
|
+
:alt="connector.name"
|
|
50
|
+
/>
|
|
51
|
+
<div
|
|
52
|
+
v-else
|
|
53
|
+
class="default-wallet-icon"
|
|
35
54
|
>
|
|
36
|
-
<Icon type="
|
|
37
|
-
|
|
38
|
-
</
|
|
39
|
-
</
|
|
40
|
-
<
|
|
41
|
-
v-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@back="resetConnection"
|
|
49
|
-
/>
|
|
50
|
-
<template v-else-if="isConnecting">
|
|
51
|
-
<Loading
|
|
52
|
-
:txt="`Waiting for ${connectingWallet} confirmation...`"
|
|
53
|
-
spinner
|
|
54
|
-
stacked
|
|
55
|
+
<Icon type="wallet" />
|
|
56
|
+
</div>
|
|
57
|
+
<span>{{ connector.name }}</span>
|
|
58
|
+
</Button>
|
|
59
|
+
<Button
|
|
60
|
+
v-if="wcConnector"
|
|
61
|
+
@click="loginWithSafe"
|
|
62
|
+
class="block choose-connector"
|
|
63
|
+
>
|
|
64
|
+
<img
|
|
65
|
+
:src="safeIcon"
|
|
66
|
+
alt="Safe"
|
|
55
67
|
/>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
<span>Safe</span>
|
|
69
|
+
</Button>
|
|
70
|
+
<Button
|
|
71
|
+
v-if="inAppConnector"
|
|
72
|
+
@click="showInAppSetup = true"
|
|
73
|
+
class="block choose-connector"
|
|
60
74
|
>
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<Icon type="wallet" />
|
|
77
|
-
</div>
|
|
78
|
-
<span>{{ connector.name }}</span>
|
|
79
|
-
</Button>
|
|
80
|
-
<Button
|
|
81
|
-
v-if="wcConnector"
|
|
82
|
-
@click="loginWithSafe"
|
|
83
|
-
class="block choose-connector"
|
|
84
|
-
>
|
|
85
|
-
<img
|
|
86
|
-
:src="safeIcon"
|
|
87
|
-
alt="Safe"
|
|
88
|
-
/>
|
|
89
|
-
<span>Safe</span>
|
|
90
|
-
</Button>
|
|
91
|
-
<Button
|
|
92
|
-
v-if="inAppConnector"
|
|
93
|
-
@click="showInAppSetup = true"
|
|
94
|
-
class="block choose-connector"
|
|
95
|
-
>
|
|
96
|
-
<img
|
|
97
|
-
:src="inAppIcon"
|
|
98
|
-
alt="Seed Phrase"
|
|
99
|
-
/>
|
|
100
|
-
<span>In App</span>
|
|
101
|
-
</Button>
|
|
102
|
-
<Button
|
|
103
|
-
to="https://ethereum.org/wallets/"
|
|
104
|
-
target="_blank"
|
|
105
|
-
class="link muted small"
|
|
106
|
-
>
|
|
107
|
-
<Icon type="help" />
|
|
108
|
-
<span>New to wallets?</span>
|
|
109
|
-
</Button>
|
|
110
|
-
</div>
|
|
111
|
-
</Dialog>
|
|
75
|
+
<img
|
|
76
|
+
:src="inAppIcon"
|
|
77
|
+
alt="Seed Phrase"
|
|
78
|
+
/>
|
|
79
|
+
<span>In App</span>
|
|
80
|
+
</Button>
|
|
81
|
+
<Button
|
|
82
|
+
to="https://ethereum.org/wallets/"
|
|
83
|
+
target="_blank"
|
|
84
|
+
class="link muted small"
|
|
85
|
+
>
|
|
86
|
+
<Icon type="help" />
|
|
87
|
+
<span>New to wallets?</span>
|
|
88
|
+
</Button>
|
|
89
|
+
</div>
|
|
112
90
|
</template>
|
|
113
91
|
|
|
114
92
|
<script setup lang="ts">
|
|
115
|
-
import { ref, computed
|
|
93
|
+
import { ref, computed } from 'vue'
|
|
116
94
|
import type { Connector } from '@wagmi/vue'
|
|
117
|
-
import {
|
|
118
|
-
useConnection,
|
|
119
|
-
useConnect,
|
|
120
|
-
useConnectors,
|
|
121
|
-
useChainId,
|
|
122
|
-
} from '@wagmi/vue'
|
|
95
|
+
import { useConnect, useConnectors, useChainId } from '@wagmi/vue'
|
|
123
96
|
import Button from '../../base/components/Button.vue'
|
|
124
|
-
import Dialog from '../../base/components/Dialog.vue'
|
|
125
97
|
import Icon from '../../base/components/Icon.vue'
|
|
126
98
|
import Alert from '../../base/components/Alert.vue'
|
|
127
99
|
import Loading from '../../base/components/Loading.vue'
|
|
128
|
-
import EvmAccount from './EvmAccount.vue'
|
|
129
100
|
import EvmMetaMaskQR from './EvmMetaMaskQR.vue'
|
|
130
101
|
import EvmWalletConnectWallets from './EvmWalletConnectWallets.vue'
|
|
131
102
|
import EvmInAppWalletSetup from './EvmInAppWalletSetup.vue'
|
|
@@ -155,24 +126,19 @@ const PRIORITY: Record<string, number> = {
|
|
|
155
126
|
'Base Account': 10,
|
|
156
127
|
}
|
|
157
128
|
|
|
158
|
-
defineProps<{
|
|
159
|
-
className?: string
|
|
160
|
-
}>()
|
|
161
129
|
const emit = defineEmits<{
|
|
162
|
-
connected: [
|
|
163
|
-
disconnected: []
|
|
130
|
+
connected: []
|
|
164
131
|
}>()
|
|
132
|
+
|
|
165
133
|
const chainId = useChainId()
|
|
166
134
|
const connectors = useConnectors()
|
|
167
135
|
const { mutateAsync: connectAsync } = useConnect()
|
|
168
|
-
const { address, isConnected } = useConnection()
|
|
169
136
|
|
|
170
137
|
const inAppConnector = computed(() =>
|
|
171
138
|
connectors.value.find((c) => c.type === 'inAppWallet'),
|
|
172
139
|
)
|
|
173
140
|
const showInAppSetup = ref(false)
|
|
174
141
|
|
|
175
|
-
const showConnect = computed(() => !isConnected.value)
|
|
176
142
|
const shownConnectors = computed(() => {
|
|
177
143
|
const unique = Array.from(
|
|
178
144
|
new Map(
|
|
@@ -199,7 +165,6 @@ const wcConnector = computed(() =>
|
|
|
199
165
|
connectors.value.find((c) => c.id === 'walletConnect'),
|
|
200
166
|
)
|
|
201
167
|
|
|
202
|
-
const chooseModalOpen = ref(false)
|
|
203
168
|
const errorMessage = ref('')
|
|
204
169
|
const isConnecting = ref(false)
|
|
205
170
|
const connectingWallet = ref('')
|
|
@@ -249,31 +214,24 @@ const login = async (connector: Connector) => {
|
|
|
249
214
|
try {
|
|
250
215
|
await connectAsync({ connector, chainId: chainId.value })
|
|
251
216
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
metaMaskUri.value = ''
|
|
256
|
-
walletConnectUri.value = ''
|
|
257
|
-
safeDeepLink.value = false
|
|
258
|
-
}, 100)
|
|
217
|
+
emit('connected')
|
|
218
|
+
|
|
219
|
+
resetConnection()
|
|
259
220
|
} catch (error: unknown) {
|
|
260
221
|
isConnecting.value = false
|
|
261
222
|
metaMaskUri.value = ''
|
|
262
223
|
walletConnectUri.value = ''
|
|
263
224
|
safeDeepLink.value = false
|
|
264
225
|
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
} else {
|
|
275
|
-
errorMessage.value = 'Failed to connect. Please try again.'
|
|
276
|
-
}
|
|
226
|
+
const errorMsg = error instanceof Error ? error.message : ''
|
|
227
|
+
if (
|
|
228
|
+
errorMsg.includes('User rejected') ||
|
|
229
|
+
errorMsg.includes('rejected') ||
|
|
230
|
+
errorMsg.includes('denied')
|
|
231
|
+
) {
|
|
232
|
+
errorMessage.value = 'Connection cancelled. Please try again.'
|
|
233
|
+
} else {
|
|
234
|
+
errorMessage.value = 'Failed to connect. Please try again.'
|
|
277
235
|
}
|
|
278
236
|
console.error('Wallet connection error:', error)
|
|
279
237
|
} finally {
|
|
@@ -295,25 +253,16 @@ const resetConnection = () => {
|
|
|
295
253
|
}
|
|
296
254
|
|
|
297
255
|
const onInAppConnected = () => {
|
|
298
|
-
chooseModalOpen.value = false
|
|
299
256
|
showInAppSetup.value = false
|
|
257
|
+
emit('connected')
|
|
300
258
|
}
|
|
301
259
|
|
|
302
|
-
const
|
|
303
|
-
chooseModalOpen.value = false
|
|
260
|
+
const reset = () => {
|
|
304
261
|
resetConnection()
|
|
305
262
|
showInAppSetup.value = false
|
|
306
263
|
}
|
|
307
264
|
|
|
308
|
-
|
|
309
|
-
isConnected.value
|
|
310
|
-
? emit('connected', { address: address.value })
|
|
311
|
-
: emit('disconnected')
|
|
312
|
-
watch(isConnected, () => {
|
|
313
|
-
check()
|
|
314
|
-
if (!isConnected.value) onModalClosed()
|
|
315
|
-
})
|
|
316
|
-
onMounted(() => check())
|
|
265
|
+
defineExpose({ reset })
|
|
317
266
|
</script>
|
|
318
267
|
|
|
319
268
|
<style scoped>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Button
|
|
3
|
+
v-if="showConnect"
|
|
4
|
+
@click="open = true"
|
|
5
|
+
:class="className"
|
|
6
|
+
>
|
|
7
|
+
<slot>Connect Wallet</slot>
|
|
8
|
+
</Button>
|
|
9
|
+
<slot
|
|
10
|
+
v-else-if="isConnected"
|
|
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="open"
|
|
21
|
+
@closed="onClosed"
|
|
22
|
+
>
|
|
23
|
+
<EvmConnect
|
|
24
|
+
ref="connectRef"
|
|
25
|
+
@connected="onConnected"
|
|
26
|
+
/>
|
|
27
|
+
</Dialog>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { ref, computed, watch, onMounted } from 'vue'
|
|
32
|
+
import { useConnection } from '@wagmi/vue'
|
|
33
|
+
import Button from '../../base/components/Button.vue'
|
|
34
|
+
import Dialog from '../../base/components/Dialog.vue'
|
|
35
|
+
import EvmAccount from './EvmAccount.vue'
|
|
36
|
+
import EvmConnect from './EvmConnect.vue'
|
|
37
|
+
|
|
38
|
+
defineProps<{
|
|
39
|
+
className?: string
|
|
40
|
+
}>()
|
|
41
|
+
|
|
42
|
+
const emit = defineEmits<{
|
|
43
|
+
connected: [{ address: `0x${string}` | undefined }]
|
|
44
|
+
disconnected: []
|
|
45
|
+
}>()
|
|
46
|
+
|
|
47
|
+
const { address, isConnected } = useConnection()
|
|
48
|
+
|
|
49
|
+
const showConnect = computed(() => !isConnected.value)
|
|
50
|
+
const open = ref(false)
|
|
51
|
+
const connectRef = ref<InstanceType<typeof EvmConnect> | null>(null)
|
|
52
|
+
|
|
53
|
+
const onConnected = () => {
|
|
54
|
+
open.value = false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const onClosed = () => {
|
|
58
|
+
connectRef.value?.reset()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const check = () =>
|
|
62
|
+
isConnected.value
|
|
63
|
+
? emit('connected', { address: address.value })
|
|
64
|
+
: emit('disconnected')
|
|
65
|
+
|
|
66
|
+
watch(isConnected, () => {
|
|
67
|
+
check()
|
|
68
|
+
if (!isConnected.value) {
|
|
69
|
+
open.value = false
|
|
70
|
+
connectRef.value?.reset()
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
onMounted(() => check())
|
|
75
|
+
</script>
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Loading
|
|
3
|
+
v-if="step === 'signing'"
|
|
4
|
+
spinner
|
|
5
|
+
stacked
|
|
6
|
+
:txt="connector?.name
|
|
7
|
+
? `Requesting signature from ${connector.name}...`
|
|
8
|
+
: 'Requesting signature...'"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<Loading
|
|
12
|
+
v-else-if="step === 'verifying'"
|
|
13
|
+
spinner
|
|
14
|
+
stacked
|
|
15
|
+
txt="Verifying signature..."
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<template v-else-if="step === 'complete'">
|
|
19
|
+
<slot name="complete">
|
|
20
|
+
<Alert type="info">
|
|
21
|
+
<p>Successfully signed in.</p>
|
|
22
|
+
</Alert>
|
|
23
|
+
</slot>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<template v-else-if="step === 'error'">
|
|
27
|
+
<Alert type="error">
|
|
28
|
+
<p>{{ errorMessage }}</p>
|
|
29
|
+
</Alert>
|
|
30
|
+
<Button
|
|
31
|
+
class="secondary"
|
|
32
|
+
@click="signIn"
|
|
33
|
+
>Try Again</Button>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<template v-else>
|
|
37
|
+
<slot name="idle">
|
|
38
|
+
<p v-if="props.statement">{{ props.statement }}</p>
|
|
39
|
+
</slot>
|
|
40
|
+
<Button @click="signIn">
|
|
41
|
+
Sign In
|
|
42
|
+
</Button>
|
|
43
|
+
</template>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
import { ref } from 'vue'
|
|
48
|
+
import { signMessage } from '@wagmi/core'
|
|
49
|
+
import { useConfig, useConnection } from '@wagmi/vue'
|
|
50
|
+
import type { Config } from '@wagmi/vue'
|
|
51
|
+
import Button from '../../base/components/Button.vue'
|
|
52
|
+
import Alert from '../../base/components/Alert.vue'
|
|
53
|
+
import Loading from '../../base/components/Loading.vue'
|
|
54
|
+
import { createSiweMessage } from '../utils/siwe'
|
|
55
|
+
import { useSiwe } from '../composables/siwe'
|
|
56
|
+
|
|
57
|
+
type Step = 'idle' | 'signing' | 'verifying' | 'complete' | 'error'
|
|
58
|
+
|
|
59
|
+
const props = defineProps<{
|
|
60
|
+
getNonce: () => Promise<string>
|
|
61
|
+
verify: (message: string, signature: string) => Promise<boolean>
|
|
62
|
+
domain?: string
|
|
63
|
+
statement?: string
|
|
64
|
+
uri?: string
|
|
65
|
+
resources?: string[]
|
|
66
|
+
requestId?: string
|
|
67
|
+
expirationTime?: string
|
|
68
|
+
}>()
|
|
69
|
+
|
|
70
|
+
const emit = defineEmits<{
|
|
71
|
+
authenticated: [{ address: `0x${string}`; chainId: number }]
|
|
72
|
+
error: [error: string]
|
|
73
|
+
}>()
|
|
74
|
+
|
|
75
|
+
function isUserRejection(e: unknown): boolean {
|
|
76
|
+
const re = /reject|denied|cancel/i
|
|
77
|
+
let current = e as Record<string, unknown> | undefined
|
|
78
|
+
while (current) {
|
|
79
|
+
if ((current as { code?: number }).code === 4001) return true
|
|
80
|
+
if (re.test((current as { details?: string }).details || '')) return true
|
|
81
|
+
if (re.test((current as { message?: string }).message || '')) return true
|
|
82
|
+
current = current.cause as Record<string, unknown> | undefined
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const config = useConfig()
|
|
88
|
+
const { address, chainId, connector } = useConnection()
|
|
89
|
+
const { setSession } = useSiwe()
|
|
90
|
+
|
|
91
|
+
const step = ref<Step>('idle')
|
|
92
|
+
const errorMessage = ref('')
|
|
93
|
+
|
|
94
|
+
const signIn = async () => {
|
|
95
|
+
const currentAddress = address.value
|
|
96
|
+
const currentChainId = chainId.value
|
|
97
|
+
|
|
98
|
+
if (!currentAddress || !currentChainId) {
|
|
99
|
+
errorMessage.value = 'Wallet not connected.'
|
|
100
|
+
step.value = 'error'
|
|
101
|
+
emit('error', errorMessage.value)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
errorMessage.value = ''
|
|
106
|
+
|
|
107
|
+
// Get nonce
|
|
108
|
+
let nonce: string
|
|
109
|
+
try {
|
|
110
|
+
nonce = await props.getNonce()
|
|
111
|
+
} catch {
|
|
112
|
+
errorMessage.value = 'Failed to get authentication nonce.'
|
|
113
|
+
step.value = 'error'
|
|
114
|
+
emit('error', errorMessage.value)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Sign message
|
|
119
|
+
step.value = 'signing'
|
|
120
|
+
const message = createSiweMessage({
|
|
121
|
+
domain: props.domain || window.location.host,
|
|
122
|
+
address: currentAddress,
|
|
123
|
+
uri: props.uri || window.location.origin,
|
|
124
|
+
chainId: currentChainId,
|
|
125
|
+
nonce,
|
|
126
|
+
statement: props.statement,
|
|
127
|
+
expirationTime: props.expirationTime,
|
|
128
|
+
requestId: props.requestId,
|
|
129
|
+
resources: props.resources,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
let signature: string
|
|
133
|
+
try {
|
|
134
|
+
signature = await signMessage(config as Config, { message })
|
|
135
|
+
} catch (e: unknown) {
|
|
136
|
+
if (isUserRejection(e)) {
|
|
137
|
+
errorMessage.value = 'Signature rejected by user.'
|
|
138
|
+
} else {
|
|
139
|
+
const err = e as { shortMessage?: string; message?: string }
|
|
140
|
+
errorMessage.value = err.shortMessage || err.message || 'Failed to sign message.'
|
|
141
|
+
}
|
|
142
|
+
step.value = 'error'
|
|
143
|
+
emit('error', errorMessage.value)
|
|
144
|
+
console.error('SIWE signing error:', e)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Verify with backend
|
|
149
|
+
step.value = 'verifying'
|
|
150
|
+
try {
|
|
151
|
+
const verified = await props.verify(message, signature)
|
|
152
|
+
|
|
153
|
+
if (!verified) {
|
|
154
|
+
throw new Error('Signature verification failed')
|
|
155
|
+
}
|
|
156
|
+
} catch (e: unknown) {
|
|
157
|
+
const err = e as { message?: string }
|
|
158
|
+
errorMessage.value = err.message || 'Verification failed.'
|
|
159
|
+
step.value = 'error'
|
|
160
|
+
emit('error', errorMessage.value)
|
|
161
|
+
console.error('SIWE verification error:', e)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Update shared authentication state
|
|
166
|
+
setSession({
|
|
167
|
+
address: currentAddress,
|
|
168
|
+
chainId: currentChainId,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
step.value = 'complete'
|
|
172
|
+
emit('authenticated', {
|
|
173
|
+
address: currentAddress,
|
|
174
|
+
chainId: currentChainId,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const reset = () => {
|
|
179
|
+
step.value = 'idle'
|
|
180
|
+
errorMessage.value = ''
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
defineExpose({ reset })
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<style scoped>
|
|
187
|
+
.secondary {
|
|
188
|
+
margin-top: var(--spacer-sm);
|
|
189
|
+
}
|
|
190
|
+
</style>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Button
|
|
3
|
+
v-if="!isAuthenticated"
|
|
4
|
+
@click="open = true"
|
|
5
|
+
:class="className"
|
|
6
|
+
>
|
|
7
|
+
<slot>Sign In</slot>
|
|
8
|
+
</Button>
|
|
9
|
+
<slot
|
|
10
|
+
v-else
|
|
11
|
+
name="authenticated"
|
|
12
|
+
:address="session?.address"
|
|
13
|
+
:sign-out="handleSignOut"
|
|
14
|
+
>
|
|
15
|
+
<Button
|
|
16
|
+
@click="handleSignOut"
|
|
17
|
+
:class="className"
|
|
18
|
+
>Sign Out</Button>
|
|
19
|
+
</slot>
|
|
20
|
+
|
|
21
|
+
<Dialog
|
|
22
|
+
v-if="!isAuthenticated"
|
|
23
|
+
title="Sign In with Ethereum"
|
|
24
|
+
v-model:open="open"
|
|
25
|
+
@closed="onClosed"
|
|
26
|
+
>
|
|
27
|
+
<EvmSiwe
|
|
28
|
+
ref="siweRef"
|
|
29
|
+
:get-nonce="getNonce"
|
|
30
|
+
:verify="verify"
|
|
31
|
+
:domain="domain"
|
|
32
|
+
:statement="statement"
|
|
33
|
+
:uri="uri"
|
|
34
|
+
:resources="resources"
|
|
35
|
+
:request-id="requestId"
|
|
36
|
+
:expiration-time="expirationTime"
|
|
37
|
+
@authenticated="onAuthenticated"
|
|
38
|
+
@error="(e) => emit('error', e)"
|
|
39
|
+
/>
|
|
40
|
+
</Dialog>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup lang="ts">
|
|
44
|
+
import { ref, watch } from 'vue'
|
|
45
|
+
import Button from '../../base/components/Button.vue'
|
|
46
|
+
import Dialog from '../../base/components/Dialog.vue'
|
|
47
|
+
import EvmSiwe from './EvmSiwe.vue'
|
|
48
|
+
import { useSiwe } from '../composables/siwe'
|
|
49
|
+
|
|
50
|
+
const props = defineProps<{
|
|
51
|
+
className?: string
|
|
52
|
+
getNonce: () => Promise<string>
|
|
53
|
+
verify: (message: string, signature: string) => Promise<boolean>
|
|
54
|
+
domain?: string
|
|
55
|
+
statement?: string
|
|
56
|
+
uri?: string
|
|
57
|
+
resources?: string[]
|
|
58
|
+
requestId?: string
|
|
59
|
+
expirationTime?: string
|
|
60
|
+
}>()
|
|
61
|
+
|
|
62
|
+
const emit = defineEmits<{
|
|
63
|
+
authenticated: [{ address: `0x${string}`; chainId: number }]
|
|
64
|
+
signedOut: []
|
|
65
|
+
error: [error: string]
|
|
66
|
+
}>()
|
|
67
|
+
|
|
68
|
+
const { isAuthenticated, session, signOut } = useSiwe()
|
|
69
|
+
|
|
70
|
+
const open = ref(false)
|
|
71
|
+
const siweRef = ref<InstanceType<typeof EvmSiwe> | null>(null)
|
|
72
|
+
|
|
73
|
+
const onAuthenticated = (data: { address: `0x${string}`; chainId: number }) => {
|
|
74
|
+
open.value = false
|
|
75
|
+
emit('authenticated', data)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const onClosed = () => {
|
|
79
|
+
siweRef.value?.reset()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handleSignOut = () => {
|
|
83
|
+
signOut()
|
|
84
|
+
emit('signedOut')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
watch(isAuthenticated, (authenticated) => {
|
|
88
|
+
if (!authenticated) {
|
|
89
|
+
open.value = false
|
|
90
|
+
siweRef.value?.reset()
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
</script>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ref, readonly } from 'vue'
|
|
2
|
+
import { signMessage } from '@wagmi/core'
|
|
3
|
+
import { useConfig, useConnection } from '@wagmi/vue'
|
|
4
|
+
import type { Config } from '@wagmi/vue'
|
|
5
|
+
import { createSiweMessage, type SiweMessageParams } from '../utils/siwe'
|
|
6
|
+
|
|
7
|
+
export interface SiweSession {
|
|
8
|
+
address: `0x${string}`
|
|
9
|
+
chainId: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseSiweOptions {
|
|
13
|
+
getNonce: () => Promise<string>
|
|
14
|
+
verify: (message: string, signature: string) => Promise<boolean>
|
|
15
|
+
domain?: string
|
|
16
|
+
uri?: string
|
|
17
|
+
statement?: string
|
|
18
|
+
expirationTime?: string
|
|
19
|
+
requestId?: string
|
|
20
|
+
resources?: string[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const _isAuthenticated = ref(false)
|
|
24
|
+
const _session = ref<SiweSession | null>(null)
|
|
25
|
+
|
|
26
|
+
export const useSiwe = () => {
|
|
27
|
+
const config = useConfig()
|
|
28
|
+
const { address, chainId } = useConnection()
|
|
29
|
+
|
|
30
|
+
const setSession = (session: SiweSession) => {
|
|
31
|
+
_isAuthenticated.value = true
|
|
32
|
+
_session.value = session
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const clearSession = () => {
|
|
36
|
+
_isAuthenticated.value = false
|
|
37
|
+
_session.value = null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const signIn = async (options: UseSiweOptions) => {
|
|
41
|
+
const currentAddress = address.value
|
|
42
|
+
const currentChainId = chainId.value
|
|
43
|
+
|
|
44
|
+
if (!currentAddress || !currentChainId) {
|
|
45
|
+
throw new Error('Wallet not connected')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const nonce = await options.getNonce()
|
|
49
|
+
|
|
50
|
+
const messageParams: SiweMessageParams = {
|
|
51
|
+
domain: options.domain || window.location.host,
|
|
52
|
+
address: currentAddress,
|
|
53
|
+
uri: options.uri || window.location.origin,
|
|
54
|
+
chainId: currentChainId,
|
|
55
|
+
nonce,
|
|
56
|
+
statement: options.statement,
|
|
57
|
+
expirationTime: options.expirationTime,
|
|
58
|
+
requestId: options.requestId,
|
|
59
|
+
resources: options.resources,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const message = createSiweMessage(messageParams)
|
|
63
|
+
const signature = await signMessage(config as Config, { message })
|
|
64
|
+
|
|
65
|
+
const verified = await options.verify(message, signature)
|
|
66
|
+
|
|
67
|
+
if (!verified) {
|
|
68
|
+
throw new Error('Signature verification failed')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setSession({
|
|
72
|
+
address: currentAddress,
|
|
73
|
+
chainId: currentChainId,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const signOut = () => {
|
|
78
|
+
clearSession()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
isAuthenticated: readonly(_isAuthenticated),
|
|
83
|
+
session: readonly(_session),
|
|
84
|
+
signIn,
|
|
85
|
+
signOut,
|
|
86
|
+
setSession,
|
|
87
|
+
clearSession,
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/evm/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type { EnsProfile } from './utils/ens'
|
|
|
18
18
|
export { stringifyJSON, parseJSON, formatPrice } from './utils/price'
|
|
19
19
|
export { resolveUri } from './utils/uri'
|
|
20
20
|
export type { ResolveUriOptions } from './utils/uri'
|
|
21
|
+
export { createSiweMessage } from './utils/siwe'
|
|
22
|
+
export type { SiweMessageParams } from './utils/siwe'
|
|
21
23
|
|
|
22
24
|
// Composables
|
|
23
25
|
export { useBaseURL } from './composables/base'
|
|
@@ -33,6 +35,8 @@ export { useGasPrice } from './composables/gasPrice'
|
|
|
33
35
|
export { usePriceFeed } from './composables/priceFeed'
|
|
34
36
|
export { useWalletExplorer } from './composables/walletExplorer'
|
|
35
37
|
export type { ExplorerWallet } from './composables/walletExplorer'
|
|
38
|
+
export { useSiwe } from './composables/siwe'
|
|
39
|
+
export type { SiweSession, UseSiweOptions } from './composables/siwe'
|
|
36
40
|
|
|
37
41
|
// Connectors
|
|
38
42
|
export { inAppWallet, prepareInAppWallet } from './connectors/inAppWallet'
|
|
@@ -41,6 +45,8 @@ export { inAppWallet, prepareInAppWallet } from './connectors/inAppWallet'
|
|
|
41
45
|
export { default as EvmAccount } from './components/EvmAccount.vue'
|
|
42
46
|
export { default as EvmAvatar } from './components/EvmAvatar.vue'
|
|
43
47
|
export { default as EvmConnect } from './components/EvmConnect.vue'
|
|
48
|
+
export { default as EvmConnectDialog } from './components/EvmConnectDialog.vue'
|
|
49
|
+
export { default as EvmConnectionStatus } from './components/EvmConnectionStatus.vue'
|
|
44
50
|
export { default as EvmProfile } from './components/EvmProfile.vue'
|
|
45
51
|
export { default as EvmSwitchNetwork } from './components/EvmSwitchNetwork.vue'
|
|
46
52
|
export { default as EvmConnectorQR } from './components/EvmConnectorQR.vue'
|
|
@@ -50,3 +56,5 @@ export { default as EvmWalletConnectWallets } from './components/EvmWalletConnec
|
|
|
50
56
|
export { default as EvmTransactionFlow } from './components/EvmTransactionFlow.vue'
|
|
51
57
|
export { default as EvmSeedPhraseInput } from './components/EvmSeedPhraseInput.vue'
|
|
52
58
|
export { default as EvmInAppWalletSetup } from './components/EvmInAppWalletSetup.vue'
|
|
59
|
+
export { default as EvmSiwe } from './components/EvmSiwe.vue'
|
|
60
|
+
export { default as EvmSiweDialog } from './components/EvmSiweDialog.vue'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface SiweMessageParams {
|
|
2
|
+
domain: string
|
|
3
|
+
address: string
|
|
4
|
+
uri: string
|
|
5
|
+
version?: string
|
|
6
|
+
chainId: number
|
|
7
|
+
nonce: string
|
|
8
|
+
issuedAt?: string
|
|
9
|
+
expirationTime?: string
|
|
10
|
+
notBefore?: string
|
|
11
|
+
requestId?: string
|
|
12
|
+
statement?: string
|
|
13
|
+
resources?: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createSiweMessage(params: SiweMessageParams): string {
|
|
17
|
+
const {
|
|
18
|
+
domain,
|
|
19
|
+
address,
|
|
20
|
+
uri,
|
|
21
|
+
version = '1',
|
|
22
|
+
chainId,
|
|
23
|
+
nonce,
|
|
24
|
+
issuedAt = new Date().toISOString(),
|
|
25
|
+
expirationTime,
|
|
26
|
+
notBefore,
|
|
27
|
+
requestId,
|
|
28
|
+
statement,
|
|
29
|
+
resources,
|
|
30
|
+
} = params
|
|
31
|
+
|
|
32
|
+
const lines: string[] = [
|
|
33
|
+
`${domain} wants you to sign in with your Ethereum account:`,
|
|
34
|
+
address,
|
|
35
|
+
'',
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
if (statement) {
|
|
39
|
+
lines.push(statement, '')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lines.push(
|
|
43
|
+
`URI: ${uri}`,
|
|
44
|
+
`Version: ${version}`,
|
|
45
|
+
`Chain ID: ${chainId}`,
|
|
46
|
+
`Nonce: ${nonce}`,
|
|
47
|
+
`Issued At: ${issuedAt}`,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if (expirationTime) {
|
|
51
|
+
lines.push(`Expiration Time: ${expirationTime}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (notBefore) {
|
|
55
|
+
lines.push(`Not Before: ${notBefore}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (requestId) {
|
|
59
|
+
lines.push(`Request ID: ${requestId}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (resources?.length) {
|
|
63
|
+
lines.push('Resources:')
|
|
64
|
+
for (const resource of resources) {
|
|
65
|
+
lines.push(`- ${resource}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return lines.join('\n')
|
|
70
|
+
}
|