@zerodev/wallet-react 0.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/_cjs/actions.js +193 -0
- package/dist/_cjs/connector.js +221 -0
- package/dist/_cjs/constants.js +4 -0
- package/dist/_cjs/hooks/useAuthenticateOAuth.js +18 -0
- package/dist/_cjs/hooks/useExportWallet.js +18 -0
- package/dist/_cjs/hooks/useLoginPasskey.js +18 -0
- package/dist/_cjs/hooks/useRefreshSession.js +18 -0
- package/dist/_cjs/hooks/useRegisterPasskey.js +18 -0
- package/dist/_cjs/hooks/useSendOTP.js +18 -0
- package/dist/_cjs/hooks/useVerifyOTP.js +18 -0
- package/dist/_cjs/index.js +23 -0
- package/dist/_cjs/oauth.js +84 -0
- package/dist/_cjs/package.json +1 -0
- package/dist/_cjs/provider.js +163 -0
- package/dist/_cjs/store.js +51 -0
- package/dist/_cjs/utils/aaUtils.js +7 -0
- package/dist/_cjs/utils/timers.js +50 -0
- package/dist/_esm/actions.js +225 -0
- package/dist/_esm/connector.js +248 -0
- package/dist/_esm/constants.js +1 -0
- package/dist/_esm/hooks/useAuthenticateOAuth.js +18 -0
- package/dist/_esm/hooks/useExportWallet.js +18 -0
- package/dist/_esm/hooks/useLoginPasskey.js +18 -0
- package/dist/_esm/hooks/useRefreshSession.js +18 -0
- package/dist/_esm/hooks/useRegisterPasskey.js +18 -0
- package/dist/_esm/hooks/useSendOTP.js +18 -0
- package/dist/_esm/hooks/useVerifyOTP.js +18 -0
- package/dist/_esm/index.js +10 -0
- package/dist/_esm/oauth.js +77 -0
- package/dist/_esm/package.json +1 -0
- package/dist/_esm/provider.js +169 -0
- package/dist/_esm/store.js +51 -0
- package/dist/_esm/utils/aaUtils.js +4 -0
- package/dist/_esm/utils/timers.js +55 -0
- package/dist/_types/actions.d.ts +124 -0
- package/dist/_types/actions.d.ts.map +1 -0
- package/dist/_types/connector.d.ts +18 -0
- package/dist/_types/connector.d.ts.map +1 -0
- package/dist/_types/constants.d.ts +2 -0
- package/dist/_types/constants.d.ts.map +1 -0
- package/dist/_types/hooks/useAuthenticateOAuth.d.ts +18 -0
- package/dist/_types/hooks/useAuthenticateOAuth.d.ts.map +1 -0
- package/dist/_types/hooks/useExportWallet.d.ts +18 -0
- package/dist/_types/hooks/useExportWallet.d.ts.map +1 -0
- package/dist/_types/hooks/useLoginPasskey.d.ts +18 -0
- package/dist/_types/hooks/useLoginPasskey.d.ts.map +1 -0
- package/dist/_types/hooks/useRefreshSession.d.ts +18 -0
- package/dist/_types/hooks/useRefreshSession.d.ts.map +1 -0
- package/dist/_types/hooks/useRegisterPasskey.d.ts +18 -0
- package/dist/_types/hooks/useRegisterPasskey.d.ts.map +1 -0
- package/dist/_types/hooks/useSendOTP.d.ts +18 -0
- package/dist/_types/hooks/useSendOTP.d.ts.map +1 -0
- package/dist/_types/hooks/useVerifyOTP.d.ts +18 -0
- package/dist/_types/hooks/useVerifyOTP.d.ts.map +1 -0
- package/dist/_types/index.d.ts +15 -0
- package/dist/_types/index.d.ts.map +1 -0
- package/dist/_types/oauth.d.ts +21 -0
- package/dist/_types/oauth.d.ts.map +1 -0
- package/dist/_types/provider.d.ts +19 -0
- package/dist/_types/provider.d.ts.map +1 -0
- package/dist/_types/store.d.ts +52 -0
- package/dist/_types/store.d.ts.map +1 -0
- package/dist/_types/utils/aaUtils.d.ts +2 -0
- package/dist/_types/utils/aaUtils.d.ts.map +1 -0
- package/dist/_types/utils/timers.d.ts +22 -0
- package/dist/_types/utils/timers.d.ts.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/package.json.type +1 -0
- package/src/actions.ts +402 -0
- package/src/connector.ts +336 -0
- package/src/constants.ts +1 -0
- package/src/hooks/useAuthenticateOAuth.ts +57 -0
- package/src/hooks/useExportWallet.ts +57 -0
- package/src/hooks/useLoginPasskey.ts +57 -0
- package/src/hooks/useRefreshSession.ts +57 -0
- package/src/hooks/useRegisterPasskey.ts +57 -0
- package/src/hooks/useSendOTP.ts +57 -0
- package/src/hooks/useVerifyOTP.ts +57 -0
- package/src/index.ts +14 -0
- package/src/oauth.ts +124 -0
- package/src/provider.ts +235 -0
- package/src/store.ts +113 -0
- package/src/utils/aaUtils.ts +5 -0
- package/src/utils/timers.ts +80 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.build.tsbuildinfo +1 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type UseMutationOptions,
|
|
5
|
+
type UseMutationResult,
|
|
6
|
+
useMutation,
|
|
7
|
+
} from '@tanstack/react-query'
|
|
8
|
+
import { type Config, type ResolvedRegister, useConfig } from 'wagmi'
|
|
9
|
+
import { verifyOTP } from '../actions.js'
|
|
10
|
+
|
|
11
|
+
type ConfigParameter<config extends Config = Config> = {
|
|
12
|
+
config?: Config | config | undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook to verify OTP code
|
|
17
|
+
*/
|
|
18
|
+
export function useVerifyOTP<
|
|
19
|
+
config extends Config = ResolvedRegister['config'],
|
|
20
|
+
context = unknown,
|
|
21
|
+
>(
|
|
22
|
+
parameters: useVerifyOTP.Parameters<config, context> = {},
|
|
23
|
+
): useVerifyOTP.ReturnType<context> {
|
|
24
|
+
const { mutation } = parameters
|
|
25
|
+
const config = useConfig(parameters)
|
|
26
|
+
|
|
27
|
+
return useMutation({
|
|
28
|
+
...mutation,
|
|
29
|
+
async mutationFn(variables: verifyOTP.Parameters) {
|
|
30
|
+
return verifyOTP(config, variables)
|
|
31
|
+
},
|
|
32
|
+
mutationKey: ['verifyOTP'],
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export declare namespace useVerifyOTP {
|
|
37
|
+
type Parameters<
|
|
38
|
+
config extends Config = Config,
|
|
39
|
+
context = unknown,
|
|
40
|
+
> = ConfigParameter<config> & {
|
|
41
|
+
mutation?:
|
|
42
|
+
| UseMutationOptions<
|
|
43
|
+
verifyOTP.ReturnType,
|
|
44
|
+
verifyOTP.ErrorType,
|
|
45
|
+
verifyOTP.Parameters,
|
|
46
|
+
context
|
|
47
|
+
>
|
|
48
|
+
| undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ReturnType<context = unknown> = UseMutationResult<
|
|
52
|
+
verifyOTP.ReturnType,
|
|
53
|
+
verifyOTP.ErrorType,
|
|
54
|
+
verifyOTP.Parameters,
|
|
55
|
+
context
|
|
56
|
+
>
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { ZeroDevWalletConnectorParams } from './connector.js'
|
|
2
|
+
export { zeroDevWallet } from './connector.js'
|
|
3
|
+
export { useAuthenticateOAuth } from './hooks/useAuthenticateOAuth.js'
|
|
4
|
+
export { useExportWallet } from './hooks/useExportWallet.js'
|
|
5
|
+
export { useLoginPasskey } from './hooks/useLoginPasskey.js'
|
|
6
|
+
export { useRefreshSession } from './hooks/useRefreshSession.js'
|
|
7
|
+
export { useRegisterPasskey } from './hooks/useRegisterPasskey.js'
|
|
8
|
+
export { useSendOTP } from './hooks/useSendOTP.js'
|
|
9
|
+
export { useVerifyOTP } from './hooks/useVerifyOTP.js'
|
|
10
|
+
export type { OAuthConfig, OAuthProvider } from './oauth.js'
|
|
11
|
+
export { OAUTH_PROVIDERS } from './oauth.js'
|
|
12
|
+
export type { ZeroDevProvider } from './provider.js'
|
|
13
|
+
export type { ZeroDevWalletState } from './store.js'
|
|
14
|
+
export { createZeroDevWalletStore } from './store.js'
|
package/src/oauth.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type Hex, sha256 } from 'viem'
|
|
2
|
+
|
|
3
|
+
export const OAUTH_PROVIDERS = {
|
|
4
|
+
GOOGLE: 'google',
|
|
5
|
+
} as const
|
|
6
|
+
|
|
7
|
+
export type OAuthProvider =
|
|
8
|
+
(typeof OAUTH_PROVIDERS)[keyof typeof OAUTH_PROVIDERS]
|
|
9
|
+
|
|
10
|
+
export type OAuthConfig = {
|
|
11
|
+
googleClientId?: string
|
|
12
|
+
redirectUri: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type OAuthFlowParams = {
|
|
16
|
+
provider: OAuthProvider
|
|
17
|
+
clientId: string
|
|
18
|
+
redirectUri: string
|
|
19
|
+
nonce: string
|
|
20
|
+
state?: Record<string, string>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
|
24
|
+
|
|
25
|
+
const POPUP_WIDTH = 500
|
|
26
|
+
const POPUP_HEIGHT = 600
|
|
27
|
+
|
|
28
|
+
export function buildOAuthUrl(params: OAuthFlowParams): string {
|
|
29
|
+
const { provider, clientId, redirectUri, nonce, state } = params
|
|
30
|
+
|
|
31
|
+
if (provider !== OAUTH_PROVIDERS.GOOGLE) {
|
|
32
|
+
throw new Error(`Unsupported OAuth provider: ${provider}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const authUrl = new URL(GOOGLE_AUTH_URL)
|
|
36
|
+
authUrl.searchParams.set('client_id', clientId)
|
|
37
|
+
authUrl.searchParams.set('redirect_uri', redirectUri)
|
|
38
|
+
authUrl.searchParams.set('response_type', 'id_token')
|
|
39
|
+
authUrl.searchParams.set('scope', 'openid email profile')
|
|
40
|
+
authUrl.searchParams.set('nonce', nonce)
|
|
41
|
+
authUrl.searchParams.set('prompt', 'select_account')
|
|
42
|
+
|
|
43
|
+
let stateParam = `provider=${provider}`
|
|
44
|
+
if (state) {
|
|
45
|
+
const additionalState = Object.entries(state)
|
|
46
|
+
.map(
|
|
47
|
+
([key, value]) =>
|
|
48
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
|
|
49
|
+
)
|
|
50
|
+
.join('&')
|
|
51
|
+
if (additionalState) {
|
|
52
|
+
stateParam += `&${additionalState}`
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
authUrl.searchParams.set('state', stateParam)
|
|
56
|
+
|
|
57
|
+
return authUrl.toString()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function openOAuthPopup(url: string): Window | null {
|
|
61
|
+
const width = POPUP_WIDTH
|
|
62
|
+
const height = POPUP_HEIGHT
|
|
63
|
+
const left = window.screenX + (window.innerWidth - width) / 2
|
|
64
|
+
const top = window.screenY + (window.innerHeight - height) / 2
|
|
65
|
+
|
|
66
|
+
const authWindow = window.open(
|
|
67
|
+
'about:blank',
|
|
68
|
+
'_blank',
|
|
69
|
+
`width=${width},height=${height},top=${top},left=${left},scrollbars=yes,resizable=yes`,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if (authWindow) {
|
|
73
|
+
authWindow.location.href = url
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return authWindow
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function extractOAuthToken(url: string): string | null {
|
|
80
|
+
const hashParams = new URLSearchParams(url.split('#')[1])
|
|
81
|
+
let idToken = hashParams.get('id_token')
|
|
82
|
+
|
|
83
|
+
if (!idToken) {
|
|
84
|
+
const queryParams = new URLSearchParams(url.split('?')[1])
|
|
85
|
+
idToken = queryParams.get('id_token')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return idToken
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function pollOAuthPopup(
|
|
92
|
+
authWindow: Window,
|
|
93
|
+
originUrl: string,
|
|
94
|
+
onSuccess: (token: string) => void,
|
|
95
|
+
onError: (error: Error) => void,
|
|
96
|
+
): void {
|
|
97
|
+
const interval = setInterval(() => {
|
|
98
|
+
try {
|
|
99
|
+
if (authWindow.closed) {
|
|
100
|
+
clearInterval(interval)
|
|
101
|
+
onError(new Error('Authentication window was closed'))
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const url = authWindow.location.href || ''
|
|
106
|
+
|
|
107
|
+
if (url.startsWith(originUrl)) {
|
|
108
|
+
const token = extractOAuthToken(url)
|
|
109
|
+
|
|
110
|
+
if (token) {
|
|
111
|
+
authWindow.close()
|
|
112
|
+
clearInterval(interval)
|
|
113
|
+
onSuccess(token)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Ignore cross-origin errors
|
|
118
|
+
}
|
|
119
|
+
}, 500)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function generateOAuthNonce(publicKey: string): string {
|
|
123
|
+
return sha256(publicKey as Hex).replace(/^0x/, '')
|
|
124
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { KernelSmartAccountImplementation } from '@zerodev/sdk'
|
|
2
|
+
import { normalizeTimestamp } from '@zerodev/wallet-core'
|
|
3
|
+
import { Provider } from 'ox'
|
|
4
|
+
import type { Chain, LocalAccount } from 'viem'
|
|
5
|
+
import type { SmartAccount } from 'viem/account-abstraction'
|
|
6
|
+
import type { ZeroDevWalletConnectorParams } from './connector.js'
|
|
7
|
+
import type { createZeroDevWalletStore } from './store.js'
|
|
8
|
+
|
|
9
|
+
const SESSION_WARNING_THRESHOLD_MS = 60 * 1000 // 1 minute before expiry
|
|
10
|
+
|
|
11
|
+
type CreateProviderParams = {
|
|
12
|
+
store: ReturnType<typeof createZeroDevWalletStore>
|
|
13
|
+
config: ZeroDevWalletConnectorParams
|
|
14
|
+
chains: Chain[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ZeroDevProvider = ReturnType<typeof Provider.createEmitter> & {
|
|
18
|
+
request(args: { method: string; params?: unknown[] }): Promise<unknown>
|
|
19
|
+
destroy(): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createProvider({
|
|
23
|
+
store,
|
|
24
|
+
config,
|
|
25
|
+
}: CreateProviderParams): ZeroDevProvider {
|
|
26
|
+
const emitter = Provider.createEmitter()
|
|
27
|
+
let sessionRefreshTimer: NodeJS.Timeout | null = null
|
|
28
|
+
|
|
29
|
+
// Session auto-refresh logic
|
|
30
|
+
const scheduleSessionRefresh = () => {
|
|
31
|
+
if (config.autoRefreshSession === false) return
|
|
32
|
+
|
|
33
|
+
const state = store.getState()
|
|
34
|
+
if (!state.session || !state.wallet) return
|
|
35
|
+
|
|
36
|
+
const expiryMs = normalizeTimestamp(state.session.expiry)
|
|
37
|
+
const now = Date.now()
|
|
38
|
+
const timeUntilExpiry = expiryMs - now
|
|
39
|
+
|
|
40
|
+
if (timeUntilExpiry <= 0) {
|
|
41
|
+
console.log('Session already expired')
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Clear existing timer
|
|
46
|
+
if (sessionRefreshTimer) {
|
|
47
|
+
clearTimeout(sessionRefreshTimer)
|
|
48
|
+
sessionRefreshTimer = null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const threshold =
|
|
52
|
+
config.sessionWarningThreshold || SESSION_WARNING_THRESHOLD_MS
|
|
53
|
+
const refreshAt = expiryMs - threshold
|
|
54
|
+
const timeUntilRefresh = refreshAt - now
|
|
55
|
+
|
|
56
|
+
if (timeUntilRefresh <= 0) {
|
|
57
|
+
console.log('Session expiring soon, refreshing immediately...')
|
|
58
|
+
refreshSessionNow()
|
|
59
|
+
} else {
|
|
60
|
+
console.log(`Scheduling session refresh in ${timeUntilRefresh}ms`)
|
|
61
|
+
sessionRefreshTimer = setTimeout(() => {
|
|
62
|
+
refreshSessionNow()
|
|
63
|
+
}, timeUntilRefresh)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const refreshSessionNow = async () => {
|
|
68
|
+
const state = store.getState()
|
|
69
|
+
if (!state.wallet || !state.session) return
|
|
70
|
+
|
|
71
|
+
console.log('Auto-refreshing session...')
|
|
72
|
+
store.getState().setIsExpiring(true)
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const newSession = await state.wallet.refreshSession(state.session.id)
|
|
76
|
+
console.log('Session refreshed successfully')
|
|
77
|
+
store.getState().setSession(newSession || null)
|
|
78
|
+
store.getState().setIsExpiring(false)
|
|
79
|
+
|
|
80
|
+
if (newSession) {
|
|
81
|
+
scheduleSessionRefresh()
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error('Session refresh failed:', err)
|
|
85
|
+
store.getState().setIsExpiring(false)
|
|
86
|
+
store.getState().clear()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Subscribe to session changes
|
|
91
|
+
const unsubscribe = store.subscribe(
|
|
92
|
+
(state) => state.session,
|
|
93
|
+
() => {
|
|
94
|
+
scheduleSessionRefresh()
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// Schedule initial refresh if session exists
|
|
99
|
+
const initialSession = store.getState().session
|
|
100
|
+
if (initialSession) {
|
|
101
|
+
scheduleSessionRefresh()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
...emitter,
|
|
106
|
+
|
|
107
|
+
destroy() {
|
|
108
|
+
// Cleanup timer and subscription
|
|
109
|
+
if (sessionRefreshTimer) {
|
|
110
|
+
clearTimeout(sessionRefreshTimer)
|
|
111
|
+
sessionRefreshTimer = null
|
|
112
|
+
}
|
|
113
|
+
unsubscribe()
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
async request({ method, params }: { method: string; params?: any[] }) {
|
|
117
|
+
const state = store.getState()
|
|
118
|
+
const activeChainId = state.chainIds[0]
|
|
119
|
+
|
|
120
|
+
switch (method) {
|
|
121
|
+
case 'eth_accounts': {
|
|
122
|
+
const account = state.kernelAccounts.get(activeChainId)
|
|
123
|
+
return account ? [account.address] : []
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case 'eth_requestAccounts': {
|
|
127
|
+
const account = state.kernelAccounts.get(activeChainId)
|
|
128
|
+
if (!account) throw new Error('Not authenticated')
|
|
129
|
+
return [account.address]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'eth_chainId': {
|
|
133
|
+
return `0x${activeChainId.toString(16)}`
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'wallet_sendTransaction':
|
|
137
|
+
case 'eth_sendTransaction': {
|
|
138
|
+
if (!params || params.length === 0) {
|
|
139
|
+
throw new Error('Missing transaction parameters')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const [tx] = params
|
|
143
|
+
const chainId = tx.chainId ? parseInt(tx.chainId, 16) : activeChainId
|
|
144
|
+
|
|
145
|
+
// Get kernel client for this chain
|
|
146
|
+
const kernelClient = store.getState().kernelClients.get(chainId)
|
|
147
|
+
if (!kernelClient) {
|
|
148
|
+
throw new Error(`No kernel client for chain ${chainId}`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Send gasless transaction (always UserOp for EIP-7702)
|
|
152
|
+
const hash = await kernelClient.sendTransaction({
|
|
153
|
+
calls: [
|
|
154
|
+
{
|
|
155
|
+
to: tx.to,
|
|
156
|
+
value: tx.value ? BigInt(tx.value) : 0n,
|
|
157
|
+
data: tx.data || '0x',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return hash
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case 'personal_sign': {
|
|
166
|
+
if (!params || params.length < 2) {
|
|
167
|
+
throw new Error('Missing sign parameters')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const [message] = params
|
|
171
|
+
let account:
|
|
172
|
+
| SmartAccount<KernelSmartAccountImplementation>
|
|
173
|
+
| LocalAccount
|
|
174
|
+
| undefined
|
|
175
|
+
| null = state.kernelAccounts.get(activeChainId)
|
|
176
|
+
if (
|
|
177
|
+
account &&
|
|
178
|
+
'isDeployed' in account &&
|
|
179
|
+
!(await account.isDeployed())
|
|
180
|
+
) {
|
|
181
|
+
account = state.eoaAccount
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!account) throw new Error('Not authenticated')
|
|
185
|
+
|
|
186
|
+
return await account.signMessage({ message })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'eth_signTypedData_v4': {
|
|
190
|
+
if (!params || params.length < 2) {
|
|
191
|
+
throw new Error('Missing typed data parameters')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const [, typedDataJson] = params
|
|
195
|
+
let account:
|
|
196
|
+
| SmartAccount<KernelSmartAccountImplementation>
|
|
197
|
+
| LocalAccount
|
|
198
|
+
| undefined
|
|
199
|
+
| null = state.kernelAccounts.get(activeChainId)
|
|
200
|
+
if (
|
|
201
|
+
account &&
|
|
202
|
+
'isDeployed' in account &&
|
|
203
|
+
!(await account.isDeployed())
|
|
204
|
+
) {
|
|
205
|
+
account = state.eoaAccount
|
|
206
|
+
}
|
|
207
|
+
if (!account) throw new Error('Not authenticated')
|
|
208
|
+
|
|
209
|
+
const typedData = JSON.parse(typedDataJson)
|
|
210
|
+
return await account.signTypedData(typedData)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case 'wallet_switchEthereumChain': {
|
|
214
|
+
if (!params || params.length === 0) {
|
|
215
|
+
throw new Error('Missing chain parameter')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const [{ chainId }] = params
|
|
219
|
+
const chainId_number = parseInt(chainId, 16)
|
|
220
|
+
|
|
221
|
+
// Update active chain
|
|
222
|
+
store.getState().setActiveChain(chainId_number)
|
|
223
|
+
|
|
224
|
+
// Emit chainChanged event
|
|
225
|
+
emitter.emit('chainChanged', chainId)
|
|
226
|
+
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
default:
|
|
231
|
+
throw new Error(`Method not supported: ${method}`)
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
KernelAccountClient,
|
|
3
|
+
KernelSmartAccountImplementation,
|
|
4
|
+
} from '@zerodev/sdk'
|
|
5
|
+
import type {
|
|
6
|
+
ZeroDevWalletSDK,
|
|
7
|
+
ZeroDevWalletSession,
|
|
8
|
+
} from '@zerodev/wallet-core'
|
|
9
|
+
import type { LocalAccount } from 'viem'
|
|
10
|
+
import type { SmartAccount } from 'viem/account-abstraction'
|
|
11
|
+
import { create } from 'zustand'
|
|
12
|
+
import { persist, subscribeWithSelector } from 'zustand/middleware'
|
|
13
|
+
import type { OAuthConfig } from './oauth.js'
|
|
14
|
+
|
|
15
|
+
export type ZeroDevWalletState = {
|
|
16
|
+
// Core
|
|
17
|
+
wallet: ZeroDevWalletSDK | null
|
|
18
|
+
eoaAccount: LocalAccount | null
|
|
19
|
+
session: ZeroDevWalletSession | null
|
|
20
|
+
|
|
21
|
+
// Multi-chain support
|
|
22
|
+
chainIds: number[] // [activeChain, ...otherChains]
|
|
23
|
+
kernelAccounts: Map<number, SmartAccount<KernelSmartAccountImplementation>>
|
|
24
|
+
kernelClients: Map<number, KernelAccountClient>
|
|
25
|
+
|
|
26
|
+
// Session expiry
|
|
27
|
+
isExpiring: boolean
|
|
28
|
+
|
|
29
|
+
// OAuth config (optional)
|
|
30
|
+
oauthConfig: OAuthConfig | null
|
|
31
|
+
|
|
32
|
+
// Actions
|
|
33
|
+
setWallet: (wallet: ZeroDevWalletSDK) => void
|
|
34
|
+
setEoaAccount: (account: LocalAccount | null) => void
|
|
35
|
+
setKernelAccount: (
|
|
36
|
+
chainId: number,
|
|
37
|
+
account: SmartAccount<KernelSmartAccountImplementation>,
|
|
38
|
+
) => void
|
|
39
|
+
setKernelClient: (chainId: number, client: KernelAccountClient) => void
|
|
40
|
+
setSession: (session: ZeroDevWalletSession | null) => void
|
|
41
|
+
setActiveChain: (chainId: number) => void
|
|
42
|
+
setIsExpiring: (isExpiring: boolean) => void
|
|
43
|
+
setOAuthConfig: (config: OAuthConfig | null) => void
|
|
44
|
+
clear: () => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const createZeroDevWalletStore = () =>
|
|
48
|
+
create<ZeroDevWalletState>()(
|
|
49
|
+
subscribeWithSelector(
|
|
50
|
+
persist(
|
|
51
|
+
(set, get) => ({
|
|
52
|
+
// Initial state
|
|
53
|
+
wallet: null,
|
|
54
|
+
eoaAccount: null,
|
|
55
|
+
session: null,
|
|
56
|
+
chainIds: [],
|
|
57
|
+
kernelAccounts: new Map(),
|
|
58
|
+
kernelClients: new Map(),
|
|
59
|
+
isExpiring: false,
|
|
60
|
+
oauthConfig: null,
|
|
61
|
+
|
|
62
|
+
// Actions
|
|
63
|
+
setWallet: (wallet) => set({ wallet }),
|
|
64
|
+
|
|
65
|
+
setEoaAccount: (account) => set({ eoaAccount: account }),
|
|
66
|
+
|
|
67
|
+
setKernelAccount: (chainId, account) => {
|
|
68
|
+
const accounts = new Map(get().kernelAccounts)
|
|
69
|
+
accounts.set(chainId, account)
|
|
70
|
+
set({ kernelAccounts: accounts })
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
setKernelClient: (chainId, client) => {
|
|
74
|
+
const clients = new Map(get().kernelClients)
|
|
75
|
+
clients.set(chainId, client)
|
|
76
|
+
set({ kernelClients: clients })
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
setSession: (session) => set({ session }),
|
|
80
|
+
|
|
81
|
+
setActiveChain: (chainId) => {
|
|
82
|
+
const { chainIds } = get()
|
|
83
|
+
// Move chainId to front, remove duplicates
|
|
84
|
+
set({
|
|
85
|
+
chainIds: [chainId, ...chainIds.filter((id) => id !== chainId)],
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
setIsExpiring: (isExpiring) => set({ isExpiring }),
|
|
90
|
+
|
|
91
|
+
setOAuthConfig: (config) => set({ oauthConfig: config }),
|
|
92
|
+
|
|
93
|
+
clear: () =>
|
|
94
|
+
set({
|
|
95
|
+
eoaAccount: null,
|
|
96
|
+
session: null,
|
|
97
|
+
kernelAccounts: new Map(),
|
|
98
|
+
kernelClients: new Map(),
|
|
99
|
+
isExpiring: false,
|
|
100
|
+
chainIds: [],
|
|
101
|
+
}),
|
|
102
|
+
}),
|
|
103
|
+
{
|
|
104
|
+
name: 'zerodev-wallet',
|
|
105
|
+
// Only persist session data, not clients or accounts
|
|
106
|
+
partialize: (state) => ({
|
|
107
|
+
session: state.session,
|
|
108
|
+
chainIds: state.chainIds,
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Timer utilities for session management
|
|
2
|
+
// Handles delays >24.8 days (setTimeout limit) by chunking
|
|
3
|
+
|
|
4
|
+
export type TimerController = { clear: () => void }
|
|
5
|
+
export type TimerMap = Record<string, TimerController>
|
|
6
|
+
|
|
7
|
+
export const MAX_DELAY_MS = 2_147_483_647 // ~24.8 days
|
|
8
|
+
|
|
9
|
+
function toIntMs(x: number) {
|
|
10
|
+
return Math.max(0, Math.floor(Number.isFinite(x) ? x : 0))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A drop-in replacement for `setTimeout` that supports arbitrarily long delays.
|
|
15
|
+
* Browsers clamp `setTimeout` delays to ~24.8 days. This helper safely schedules
|
|
16
|
+
* timeouts that can be months or years into the future by chunking.
|
|
17
|
+
*/
|
|
18
|
+
export function setCappedTimeout(
|
|
19
|
+
cb: () => void,
|
|
20
|
+
delayMs: number,
|
|
21
|
+
): TimerController {
|
|
22
|
+
const target = Date.now() + toIntMs(delayMs)
|
|
23
|
+
let handle: ReturnType<typeof setTimeout> | undefined
|
|
24
|
+
|
|
25
|
+
const tick = () => {
|
|
26
|
+
const remaining = target - Date.now()
|
|
27
|
+
if (remaining <= 0) {
|
|
28
|
+
cb()
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
handle = setTimeout(tick, Math.min(MAX_DELAY_MS, remaining))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
tick()
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
clear() {
|
|
38
|
+
if (handle !== undefined) {
|
|
39
|
+
clearTimeout(handle)
|
|
40
|
+
handle = undefined
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Replace any existing timer for `key` with `controller`. */
|
|
47
|
+
export function putTimer(
|
|
48
|
+
map: TimerMap,
|
|
49
|
+
key: string,
|
|
50
|
+
controller: TimerController,
|
|
51
|
+
) {
|
|
52
|
+
map[key]?.clear?.()
|
|
53
|
+
map[key] = controller
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Clear a specific key (noop if missing). */
|
|
57
|
+
export function clearKey(map: TimerMap, key: string) {
|
|
58
|
+
map[key]?.clear?.()
|
|
59
|
+
delete map[key]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Clear all timers in the map. */
|
|
63
|
+
export function clearAll(map: TimerMap) {
|
|
64
|
+
for (const k of Object.keys(map)) {
|
|
65
|
+
map[k]?.clear?.()
|
|
66
|
+
delete map[k]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Convenience: set a capped timeout directly into the map for `key`.
|
|
72
|
+
*/
|
|
73
|
+
export function setCappedTimeoutInMap(
|
|
74
|
+
map: TimerMap,
|
|
75
|
+
key: string,
|
|
76
|
+
cb: () => void,
|
|
77
|
+
delayMs: number,
|
|
78
|
+
) {
|
|
79
|
+
putTimer(map, key, setCappedTimeout(cb, delayMs))
|
|
80
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../templates/typescript/tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"skipLibCheck": true
|
|
7
|
+
},
|
|
8
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
|
9
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
|
|
10
|
+
}
|