-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconnector.ts
More file actions
434 lines (372 loc) · 13.9 KB
/
connector.ts
File metadata and controls
434 lines (372 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
import { type CreateConnectorFn, createConnector } from '@wagmi/core'
import {
createKernelAccount,
createKernelAccountClient,
createZeroDevPaymasterClient,
} from '@zerodev/sdk'
import { getEntryPoint, KERNEL_V3_3 } from '@zerodev/sdk/constants'
import type {
ApiKeyStamper,
PasskeyStamper,
StorageAdapter,
} from '@zerodev/wallet-core'
import { createZeroDevWallet, KMS_SERVER_URL } from '@zerodev/wallet-core'
import { type Chain, createPublicClient, http } from 'viem'
import { handleOAuthCallback, type OAuthProvider } from './oauth.js'
import { createProvider } from './provider.js'
import { type CreateStoreOptions, createZeroDevWalletStore } from './store.js'
import { getAAUrl } from './utils/aaUtils.js'
// OAuth URL parameter used to detect callback
const OAUTH_SUCCESS_PARAM = 'oauth_success'
const OAUTH_PROVIDER_PARAM = 'oauth_provider'
const OAUTH_SESSION_ID_PARAM = 'session_id'
/**
* Detect OAuth callback from URL params and handle it.
* - If in popup: sends postMessage to opener and closes
* - If not in popup: completes auth directly
*/
async function detectAndHandleOAuthCallback(
wallet: Awaited<ReturnType<typeof createZeroDevWallet>>,
store: ReturnType<typeof createZeroDevWalletStore>,
): Promise<boolean> {
if (typeof window === 'undefined' || !window.location?.search) return false
const params = new URLSearchParams(window.location.search)
const isOAuthCallback = params.get(OAUTH_SUCCESS_PARAM) === 'true'
if (!isOAuthCallback) return false
// If in popup, use the existing handler to notify opener
if (window.opener) {
handleOAuthCallback(OAUTH_SUCCESS_PARAM)
return true
}
// Not in popup - complete auth directly (redirect flow)
console.log('OAuth callback detected, completing authentication...')
const provider = (params.get(OAUTH_PROVIDER_PARAM) ||
'google') as OAuthProvider
const sessionId = params.get(OAUTH_SESSION_ID_PARAM) || ''
try {
await wallet.auth({ type: 'oauth', provider, sessionId })
const [session, eoaAccount] = await Promise.all([
wallet.getSession(),
wallet.toAccount(),
])
store.getState().setEoaAccount(eoaAccount)
store.getState().setSession(session || null)
// Clean up URL params
params.delete(OAUTH_SUCCESS_PARAM)
params.delete(OAUTH_PROVIDER_PARAM)
params.delete(OAUTH_SESSION_ID_PARAM)
const newUrl = params.toString()
? `${window.location.pathname}?${params.toString()}`
: window.location.pathname
window.history.replaceState({}, '', newUrl)
console.log('OAuth authentication completed')
return true
} catch (error) {
console.error('OAuth authentication failed:', error)
return false
}
}
export type ZeroDevWalletConnectorParams = {
projectId: string
organizationId?: string
proxyBaseUrl?: string
aaUrl?: string // Bundler/paymaster URL
chains: readonly Chain[]
rpId?: string
sessionStorage?: StorageAdapter
persistStorage?: CreateStoreOptions['storage']
apiKeyStamper?: Promise<ApiKeyStamper>
passkeyStamper?: Promise<PasskeyStamper>
autoRefreshSession?: boolean
sessionWarningThreshold?: number
// Controls whether the connector should initialize
// On Web: pass `typeof window !== 'undefined'` to only initialize on client side
shouldInitialize?: boolean | (() => boolean)
}
export function zeroDevWallet(
params: ZeroDevWalletConnectorParams,
): CreateConnectorFn {
type Provider = ReturnType<typeof createProvider>
type Properties = {
connect<withCapabilities extends boolean = false>(parameters?: {
chainId?: number | undefined
isReconnecting?: boolean | undefined
withCapabilities?: withCapabilities | boolean | undefined
}): Promise<{
accounts: withCapabilities extends true
? readonly {
address: `0x${string}`
capabilities: Record<string, unknown>
}[]
: readonly `0x${string}`[]
chainId: number
}>
getStore(): Promise<ReturnType<typeof createZeroDevWalletStore>>
}
return createConnector<Provider, Properties>((wagmiConfig) => {
let store: ReturnType<typeof createZeroDevWalletStore>
let provider: ReturnType<typeof createProvider>
let initPromise: Promise<void> | null = null
// Get transports from Wagmi config (uses user's RPC URLs)
const transports = wagmiConfig.transports
// Lazy initialization - only runs on client side (idempotent)
const initialize = async () => {
if (initPromise) return initPromise
initPromise = doInitialize()
return initPromise
}
const doInitialize = async () => {
console.log('Initializing ZeroDevWallet connector...')
let apiKeyStamper: ApiKeyStamper | undefined
let passkeyStamper: PasskeyStamper | undefined
if (params.apiKeyStamper) {
apiKeyStamper = await params.apiKeyStamper
}
if (params.passkeyStamper) {
passkeyStamper = await params.passkeyStamper
}
// Initialize wallet SDK
const wallet = await createZeroDevWallet({
projectId: params.projectId,
...(params.organizationId && {
organizationId: params.organizationId,
}),
...(params.proxyBaseUrl && { proxyBaseUrl: params.proxyBaseUrl }),
...(params.sessionStorage && {
sessionStorage: params.sessionStorage,
}),
...(params.rpId && { rpId: params.rpId }),
...(apiKeyStamper && { apiKeyStamper }),
...(passkeyStamper && { passkeyStamper }),
})
// Create store
store = createZeroDevWalletStore({
...(params.persistStorage && { storage: params.persistStorage }),
})
store = createZeroDevWalletStore()
store.getState().setWallet(wallet)
// Store OAuth config - uses proxyBaseUrl and projectId from params
store.getState().setOAuthConfig({
backendUrl: params.proxyBaseUrl || `${KMS_SERVER_URL}/api/v1`,
projectId: params.projectId,
})
// Create EIP-1193 provider
provider = createProvider({
store,
config: params,
chains: Array.from(params.chains),
})
// Check for existing session (page reload)
const session = await wallet.getSession()
if (session) {
console.log('Found existing session, restoring...')
const eoaAccount = await wallet.toAccount()
store.getState().setEoaAccount(eoaAccount)
store.getState().setSession(session)
}
// Auto-detect OAuth callback (when popup redirects back with ?oauth_success=true)
await detectAndHandleOAuthCallback(wallet, store)
console.log('ZeroDevWallet connector initialized')
}
return {
id: 'zerodev-wallet',
name: 'ZeroDevWallet',
type: 'injected' as const,
async setup() {
const shouldInit =
typeof params.shouldInitialize === 'function'
? params.shouldInitialize()
: (params.shouldInitialize ?? typeof window !== 'undefined')
if (shouldInit) {
await initialize()
}
},
async connect({ chainId, ...rest } = {}) {
const withCapabilities =
('withCapabilities' in rest && rest.withCapabilities) || false
const isReconnecting =
('isReconnecting' in rest && rest.isReconnecting) || false
// Ensure wallet is initialized (lazy init on first connect)
await initialize()
console.log(
isReconnecting
? 'Reconnecting ZeroDevWallet...'
: 'Connecting ZeroDevWallet...',
)
const state = store.getState()
// Determine active chain
const activeChainId =
chainId ?? state.activeChainId ?? params.chains[0].id
// If reconnecting and already have kernel account, return immediately
if (isReconnecting && state.kernelAccounts.has(activeChainId)) {
const kernelAccount = state.kernelAccounts.get(activeChainId)
if (kernelAccount?.address) {
console.log('Already connected:', kernelAccount.address)
return {
accounts: [kernelAccount.address] as never,
chainId: activeChainId,
}
}
}
if (!state.eoaAccount) {
throw new Error(
'Not authenticated. Please authenticate first using passkey, OAuth, or OTP.',
)
}
// Create KernelAccount for this chain if doesn't exist
if (!state.kernelAccounts.has(activeChainId)) {
const chain = params.chains.find((c) => c.id === activeChainId)
if (!chain) {
throw new Error(`Chain ${activeChainId} not found in config`)
}
// Use transport from Wagmi config (has user's RPC URL)
const transport = transports?.[activeChainId] ?? http()
const publicClient = createPublicClient({
chain,
transport,
})
console.log(`Creating kernel account for chain ${activeChainId}...`)
const kernelAccount = await createKernelAccount(publicClient, {
entryPoint: getEntryPoint('0.7'),
kernelVersion: KERNEL_V3_3,
eip7702Account: state.eoaAccount,
})
// Store kernel account for this chain
store.getState().setKernelAccount(activeChainId, kernelAccount)
// Create and store kernel client for transactions
const kernelClient = createKernelAccountClient({
account: kernelAccount,
bundlerTransport: http(
getAAUrl(params.projectId, activeChainId, params.aaUrl),
),
chain,
client: publicClient,
paymaster: createZeroDevPaymasterClient({
chain,
transport: http(
getAAUrl(params.projectId, activeChainId, params.aaUrl),
),
}),
})
store.getState().setKernelClient(activeChainId, kernelClient)
}
// Set as active chain
store.getState().setActiveChainId(activeChainId)
// Get fresh state after updates
const freshState = store.getState()
const kernelAccount = freshState.kernelAccounts.get(activeChainId)!
console.log('ZeroDevWallet connected:', kernelAccount.address)
const address = kernelAccount.address
return {
accounts: (withCapabilities
? [{ address, capabilities: {} }]
: [address]) as never,
chainId: activeChainId,
}
},
async disconnect() {
console.log('Disconnecting ZeroDevWallet...')
if (!store) return
const wallet = store.getState().wallet
// Cleanup provider (clears timers)
provider?.destroy()
await wallet?.logout()
store.getState().clear()
},
async getAccounts() {
if (!store) return []
const { eoaAccount, kernelAccounts, activeChainId } = store.getState()
// Return EOA address if we have it (EIP-7702: EOA address = kernel address)
if (eoaAccount) {
return [eoaAccount.address]
}
// Fallback: check kernel accounts
const activeAccount = activeChainId
? kernelAccounts.get(activeChainId)
: null
return activeAccount ? [activeAccount.address] : []
},
async getChainId() {
if (!store) return params.chains[0].id
return store.getState().activeChainId ?? params.chains[0].id
},
async getProvider() {
if (!provider) {
await initialize()
}
return provider
},
async switchChain({ chainId }) {
console.log(`Switching to chain ${chainId}...`)
const state = store.getState()
if (!state.eoaAccount) {
throw new Error('Not authenticated')
}
// Update active chain
store.getState().setActiveChainId(chainId)
// Create kernel account for new chain if doesn't exist
if (!state.kernelAccounts.has(chainId)) {
const chain = params.chains.find((c) => c.id === chainId)
if (!chain) {
throw new Error(`Chain ${chainId} not found in config`)
}
// Use transport from Wagmi config (has user's RPC URL)
const transport = transports?.[chainId] ?? http()
const publicClient = createPublicClient({
chain,
transport,
})
console.log(`Creating kernel account for chain ${chainId}...`)
const kernelAccount = await createKernelAccount(publicClient, {
entryPoint: getEntryPoint('0.7'),
kernelVersion: KERNEL_V3_3,
eip7702Account: state.eoaAccount,
})
store.getState().setKernelAccount(chainId, kernelAccount)
const kernelClient = createKernelAccountClient({
account: kernelAccount,
bundlerTransport: http(
getAAUrl(params.projectId, chainId, params.aaUrl),
),
chain,
client: publicClient,
paymaster: createZeroDevPaymasterClient({
chain,
transport: http(
getAAUrl(params.projectId, chainId, params.aaUrl),
),
}),
})
store.getState().setKernelClient(chainId, kernelClient)
}
wagmiConfig.emitter.emit('change', { chainId })
return params.chains.find((c) => c.id === chainId)!
},
async isAuthorized() {
// Just check if we have a session - don't initialize here (too slow)
if (!store) return false
return !!store.getState().eoaAccount
},
// Custom method for hooks to access store
async getStore() {
await initialize()
return store
},
// Event listeners
onAccountsChanged() {
// Not applicable for this wallet type
},
onChainChanged() {
// Handled by Wagmi
},
onConnect() {
// Handled by Wagmi
},
onDisconnect() {
console.log('Disconnect event')
provider?.destroy()
store.getState().clear()
},
}
})
}