@1001-digital/components 1.2.0 → 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
CHANGED
|
@@ -214,10 +214,9 @@ const login = async (connector: Connector) => {
|
|
|
214
214
|
try {
|
|
215
215
|
await connectAsync({ connector, chainId: chainId.value })
|
|
216
216
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}, 100)
|
|
217
|
+
emit('connected')
|
|
218
|
+
|
|
219
|
+
resetConnection()
|
|
221
220
|
} catch (error: unknown) {
|
|
222
221
|
isConnecting.value = false
|
|
223
222
|
metaMaskUri.value = ''
|
|
@@ -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'
|
|
@@ -52,3 +56,5 @@ export { default as EvmWalletConnectWallets } from './components/EvmWalletConnec
|
|
|
52
56
|
export { default as EvmTransactionFlow } from './components/EvmTransactionFlow.vue'
|
|
53
57
|
export { default as EvmSeedPhraseInput } from './components/EvmSeedPhraseInput.vue'
|
|
54
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
|
+
}
|