@zerodev/wallet-react 0.0.1-alpha.10 → 0.0.1-alpha.11
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 +7 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/actions.test.ts +895 -0
- package/src/oauth.test.ts +445 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
import type { Config, Connector } from '@wagmi/core'
|
|
2
|
+
import { connect as wagmiConnect } from '@wagmi/core/actions'
|
|
3
|
+
import {
|
|
4
|
+
createIframeStamper,
|
|
5
|
+
exportPrivateKey as exportPrivateKeySdk,
|
|
6
|
+
exportWallet as exportWalletSdk,
|
|
7
|
+
} from '@zerodev/wallet-core'
|
|
8
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
9
|
+
import {
|
|
10
|
+
authenticateOAuth,
|
|
11
|
+
exportPrivateKey,
|
|
12
|
+
exportWallet,
|
|
13
|
+
getUserEmail,
|
|
14
|
+
loginPasskey,
|
|
15
|
+
refreshSession,
|
|
16
|
+
registerPasskey,
|
|
17
|
+
sendOTP,
|
|
18
|
+
verifyOTP,
|
|
19
|
+
} from './actions.js'
|
|
20
|
+
|
|
21
|
+
// Mock wagmi connect
|
|
22
|
+
vi.mock('@wagmi/core/actions', () => ({
|
|
23
|
+
connect: vi.fn().mockResolvedValue({}),
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
// Mock OAuth utilities
|
|
27
|
+
vi.mock('./oauth.js', () => ({
|
|
28
|
+
buildBackendOAuthUrl: vi.fn().mockReturnValue('https://oauth.example.com'),
|
|
29
|
+
openOAuthPopup: vi.fn(),
|
|
30
|
+
listenForOAuthMessage: vi.fn(),
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
// Mock core SDK
|
|
34
|
+
vi.mock('@zerodev/wallet-core', () => ({
|
|
35
|
+
createIframeStamper: vi.fn(),
|
|
36
|
+
exportPrivateKey: vi.fn(),
|
|
37
|
+
exportWallet: vi.fn(),
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
// Create a mock wallet instance
|
|
41
|
+
function createMockWallet() {
|
|
42
|
+
return {
|
|
43
|
+
auth: vi.fn().mockResolvedValue({ otpId: 'otp-123' }),
|
|
44
|
+
getSession: vi
|
|
45
|
+
.fn()
|
|
46
|
+
.mockResolvedValue({ id: 'session-123', expiry: 9999999999 }),
|
|
47
|
+
toAccount: vi.fn().mockResolvedValue({ address: '0x1234abcd' }),
|
|
48
|
+
getPublicKey: vi.fn().mockResolvedValue('03abcdef123'),
|
|
49
|
+
refreshSession: vi.fn().mockResolvedValue({ id: 'new-session-456' }),
|
|
50
|
+
client: {
|
|
51
|
+
getUserEmail: vi.fn().mockResolvedValue({ email: 'user@example.com' }),
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create a mock store
|
|
57
|
+
function createMockStore(
|
|
58
|
+
wallet: ReturnType<typeof createMockWallet> | null = createMockWallet(),
|
|
59
|
+
session: { id: string } | null = null,
|
|
60
|
+
oauthConfig: { backendUrl: string; projectId: string } | null = {
|
|
61
|
+
backendUrl: 'https://api.example.com',
|
|
62
|
+
projectId: 'proj-123',
|
|
63
|
+
},
|
|
64
|
+
) {
|
|
65
|
+
const state = {
|
|
66
|
+
wallet,
|
|
67
|
+
session,
|
|
68
|
+
oauthConfig,
|
|
69
|
+
setEoaAccount: vi.fn(),
|
|
70
|
+
setSession: vi.fn(),
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
getState: vi.fn().mockReturnValue(state),
|
|
74
|
+
subscribe: vi.fn(),
|
|
75
|
+
setState: vi.fn(),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create a mock connector
|
|
80
|
+
function createMockConnector(store = createMockStore()) {
|
|
81
|
+
return {
|
|
82
|
+
id: 'zerodev-wallet',
|
|
83
|
+
name: 'ZeroDev Wallet',
|
|
84
|
+
type: 'zerodev-wallet',
|
|
85
|
+
getStore: vi.fn().mockResolvedValue(store),
|
|
86
|
+
} as unknown as Connector
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Create a mock config
|
|
90
|
+
function createMockConfig(connector = createMockConnector()) {
|
|
91
|
+
return {
|
|
92
|
+
connectors: [connector],
|
|
93
|
+
chains: [],
|
|
94
|
+
state: { chainId: 1, connections: new Map() },
|
|
95
|
+
storage: null,
|
|
96
|
+
} as unknown as Config
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('React Actions', () => {
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
vi.clearAllMocks()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('registerPasskey', () => {
|
|
105
|
+
it('calls wallet.auth with passkey register mode', async () => {
|
|
106
|
+
const wallet = createMockWallet()
|
|
107
|
+
const store = createMockStore(wallet)
|
|
108
|
+
const connector = createMockConnector(store)
|
|
109
|
+
const config = createMockConfig(connector)
|
|
110
|
+
|
|
111
|
+
await registerPasskey(config, { email: 'user@example.com' })
|
|
112
|
+
|
|
113
|
+
expect(wallet.auth).toHaveBeenCalledWith({
|
|
114
|
+
type: 'passkey',
|
|
115
|
+
email: 'user@example.com',
|
|
116
|
+
mode: 'register',
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('sets session and account with correct values after registration', async () => {
|
|
121
|
+
const wallet = createMockWallet()
|
|
122
|
+
wallet.getSession.mockResolvedValue({ id: 'sess-abc', expiry: 999 })
|
|
123
|
+
wallet.toAccount.mockResolvedValue({ address: '0xdeadbeef' })
|
|
124
|
+
const store = createMockStore(wallet)
|
|
125
|
+
const connector = createMockConnector(store)
|
|
126
|
+
const config = createMockConfig(connector)
|
|
127
|
+
|
|
128
|
+
await registerPasskey(config, { email: 'user@example.com' })
|
|
129
|
+
|
|
130
|
+
expect(store.getState().setEoaAccount).toHaveBeenCalledWith({
|
|
131
|
+
address: '0xdeadbeef',
|
|
132
|
+
})
|
|
133
|
+
expect(store.getState().setSession).toHaveBeenCalledWith({
|
|
134
|
+
id: 'sess-abc',
|
|
135
|
+
expiry: 999,
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('sets session to null when getSession returns falsy', async () => {
|
|
140
|
+
const wallet = createMockWallet()
|
|
141
|
+
wallet.getSession.mockResolvedValue(null)
|
|
142
|
+
const store = createMockStore(wallet)
|
|
143
|
+
const connector = createMockConnector(store)
|
|
144
|
+
const config = createMockConfig(connector)
|
|
145
|
+
|
|
146
|
+
await registerPasskey(config, { email: 'user@example.com' })
|
|
147
|
+
|
|
148
|
+
expect(store.getState().setSession).toHaveBeenCalledWith(null)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('calls wagmiConnect after registration', async () => {
|
|
152
|
+
const wallet = createMockWallet()
|
|
153
|
+
const store = createMockStore(wallet)
|
|
154
|
+
const connector = createMockConnector(store)
|
|
155
|
+
const config = createMockConfig(connector)
|
|
156
|
+
|
|
157
|
+
await registerPasskey(config, { email: 'user@example.com' })
|
|
158
|
+
|
|
159
|
+
expect(wagmiConnect).toHaveBeenCalledWith(config, { connector })
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('throws when wallet is not initialized', async () => {
|
|
163
|
+
const store = createMockStore(null)
|
|
164
|
+
const connector = createMockConnector(store)
|
|
165
|
+
const config = createMockConfig(connector)
|
|
166
|
+
|
|
167
|
+
await expect(
|
|
168
|
+
registerPasskey(config, { email: 'user@example.com' }),
|
|
169
|
+
).rejects.toThrow('Wallet not initialized')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('uses provided connector instead of finding one', async () => {
|
|
173
|
+
const wallet = createMockWallet()
|
|
174
|
+
const store = createMockStore(wallet)
|
|
175
|
+
const customConnector = createMockConnector(store)
|
|
176
|
+
const config = createMockConfig()
|
|
177
|
+
|
|
178
|
+
await registerPasskey(config, {
|
|
179
|
+
email: 'user@example.com',
|
|
180
|
+
connector: customConnector,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
expect(customConnector.getStore).toHaveBeenCalled()
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('loginPasskey', () => {
|
|
188
|
+
it('calls wallet.auth with passkey login mode', async () => {
|
|
189
|
+
const wallet = createMockWallet()
|
|
190
|
+
const store = createMockStore(wallet)
|
|
191
|
+
const connector = createMockConnector(store)
|
|
192
|
+
const config = createMockConfig(connector)
|
|
193
|
+
|
|
194
|
+
await loginPasskey(config, { email: 'user@example.com' })
|
|
195
|
+
|
|
196
|
+
expect(wallet.auth).toHaveBeenCalledWith({
|
|
197
|
+
type: 'passkey',
|
|
198
|
+
email: 'user@example.com',
|
|
199
|
+
mode: 'login',
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('sets session and account with correct values after login', async () => {
|
|
204
|
+
const wallet = createMockWallet()
|
|
205
|
+
wallet.getSession.mockResolvedValue({ id: 'login-sess' })
|
|
206
|
+
wallet.toAccount.mockResolvedValue({ address: '0xabc' })
|
|
207
|
+
const store = createMockStore(wallet)
|
|
208
|
+
const connector = createMockConnector(store)
|
|
209
|
+
const config = createMockConfig(connector)
|
|
210
|
+
|
|
211
|
+
await loginPasskey(config, { email: 'user@example.com' })
|
|
212
|
+
|
|
213
|
+
expect(store.getState().setEoaAccount).toHaveBeenCalledWith({
|
|
214
|
+
address: '0xabc',
|
|
215
|
+
})
|
|
216
|
+
expect(store.getState().setSession).toHaveBeenCalledWith({
|
|
217
|
+
id: 'login-sess',
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('calls wagmiConnect after login', async () => {
|
|
222
|
+
const wallet = createMockWallet()
|
|
223
|
+
const store = createMockStore(wallet)
|
|
224
|
+
const connector = createMockConnector(store)
|
|
225
|
+
const config = createMockConfig(connector)
|
|
226
|
+
|
|
227
|
+
await loginPasskey(config, { email: 'user@example.com' })
|
|
228
|
+
|
|
229
|
+
expect(wagmiConnect).toHaveBeenCalledWith(config, { connector })
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('throws when wallet is not initialized', async () => {
|
|
233
|
+
const store = createMockStore(null)
|
|
234
|
+
const connector = createMockConnector(store)
|
|
235
|
+
const config = createMockConfig(connector)
|
|
236
|
+
|
|
237
|
+
await expect(
|
|
238
|
+
loginPasskey(config, { email: 'user@example.com' }),
|
|
239
|
+
).rejects.toThrow('Wallet not initialized')
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('sendOTP', () => {
|
|
244
|
+
it('calls wallet.auth with otp sendOtp mode', async () => {
|
|
245
|
+
const wallet = createMockWallet()
|
|
246
|
+
const store = createMockStore(wallet)
|
|
247
|
+
const connector = createMockConnector(store)
|
|
248
|
+
const config = createMockConfig(connector)
|
|
249
|
+
|
|
250
|
+
await sendOTP(config, { email: 'user@example.com' })
|
|
251
|
+
|
|
252
|
+
expect(wallet.auth).toHaveBeenCalledWith({
|
|
253
|
+
type: 'otp',
|
|
254
|
+
mode: 'sendOtp',
|
|
255
|
+
email: 'user@example.com',
|
|
256
|
+
contact: { type: 'email', contact: 'user@example.com' },
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('does not include emailCustomization when not provided', async () => {
|
|
261
|
+
const wallet = createMockWallet()
|
|
262
|
+
const store = createMockStore(wallet)
|
|
263
|
+
const connector = createMockConnector(store)
|
|
264
|
+
const config = createMockConfig(connector)
|
|
265
|
+
|
|
266
|
+
await sendOTP(config, { email: 'user@example.com' })
|
|
267
|
+
|
|
268
|
+
const authCall = wallet.auth.mock.calls[0][0]
|
|
269
|
+
expect(authCall).not.toHaveProperty('emailCustomization')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('returns otpId from wallet auth', async () => {
|
|
273
|
+
const wallet = createMockWallet()
|
|
274
|
+
wallet.auth.mockResolvedValue({ otpId: 'otp-456' })
|
|
275
|
+
const store = createMockStore(wallet)
|
|
276
|
+
const connector = createMockConnector(store)
|
|
277
|
+
const config = createMockConfig(connector)
|
|
278
|
+
|
|
279
|
+
const result = await sendOTP(config, { email: 'user@example.com' })
|
|
280
|
+
|
|
281
|
+
expect(result).toEqual({ otpId: 'otp-456' })
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('includes email customization when provided', async () => {
|
|
285
|
+
const wallet = createMockWallet()
|
|
286
|
+
const store = createMockStore(wallet)
|
|
287
|
+
const connector = createMockConnector(store)
|
|
288
|
+
const config = createMockConfig(connector)
|
|
289
|
+
|
|
290
|
+
await sendOTP(config, {
|
|
291
|
+
email: 'user@example.com',
|
|
292
|
+
emailCustomization: { magicLinkTemplate: 'https://app.com/verify/%s' },
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
expect(wallet.auth).toHaveBeenCalledWith({
|
|
296
|
+
type: 'otp',
|
|
297
|
+
mode: 'sendOtp',
|
|
298
|
+
email: 'user@example.com',
|
|
299
|
+
contact: { type: 'email', contact: 'user@example.com' },
|
|
300
|
+
emailCustomization: {
|
|
301
|
+
magicLinkTemplate: 'https://app.com/verify/%s',
|
|
302
|
+
},
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('throws when wallet is not initialized', async () => {
|
|
307
|
+
const store = createMockStore(null)
|
|
308
|
+
const connector = createMockConnector(store)
|
|
309
|
+
const config = createMockConfig(connector)
|
|
310
|
+
|
|
311
|
+
await expect(
|
|
312
|
+
sendOTP(config, { email: 'user@example.com' }),
|
|
313
|
+
).rejects.toThrow('Wallet not initialized')
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
describe('verifyOTP', () => {
|
|
318
|
+
it('calls wallet.auth with otp verifyOtp mode', async () => {
|
|
319
|
+
const wallet = createMockWallet()
|
|
320
|
+
const store = createMockStore(wallet)
|
|
321
|
+
const connector = createMockConnector(store)
|
|
322
|
+
const config = createMockConfig(connector)
|
|
323
|
+
|
|
324
|
+
await verifyOTP(config, { code: '123456', otpId: 'otp-123' })
|
|
325
|
+
|
|
326
|
+
expect(wallet.auth).toHaveBeenCalledWith({
|
|
327
|
+
type: 'otp',
|
|
328
|
+
mode: 'verifyOtp',
|
|
329
|
+
otpId: 'otp-123',
|
|
330
|
+
otpCode: '123456',
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('sets session and account with correct values after verification', async () => {
|
|
335
|
+
const wallet = createMockWallet()
|
|
336
|
+
wallet.getSession.mockResolvedValue({ id: 'verify-sess' })
|
|
337
|
+
wallet.toAccount.mockResolvedValue({ address: '0xverified' })
|
|
338
|
+
const store = createMockStore(wallet)
|
|
339
|
+
const connector = createMockConnector(store)
|
|
340
|
+
const config = createMockConfig(connector)
|
|
341
|
+
|
|
342
|
+
await verifyOTP(config, { code: '123456', otpId: 'otp-123' })
|
|
343
|
+
|
|
344
|
+
expect(store.getState().setEoaAccount).toHaveBeenCalledWith({
|
|
345
|
+
address: '0xverified',
|
|
346
|
+
})
|
|
347
|
+
expect(store.getState().setSession).toHaveBeenCalledWith({
|
|
348
|
+
id: 'verify-sess',
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('calls wagmiConnect after verification', async () => {
|
|
353
|
+
const wallet = createMockWallet()
|
|
354
|
+
const store = createMockStore(wallet)
|
|
355
|
+
const connector = createMockConnector(store)
|
|
356
|
+
const config = createMockConfig(connector)
|
|
357
|
+
|
|
358
|
+
await verifyOTP(config, { code: '123456', otpId: 'otp-123' })
|
|
359
|
+
|
|
360
|
+
expect(wagmiConnect).toHaveBeenCalledWith(config, { connector })
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('throws when wallet is not initialized', async () => {
|
|
364
|
+
const store = createMockStore(null)
|
|
365
|
+
const connector = createMockConnector(store)
|
|
366
|
+
const config = createMockConfig(connector)
|
|
367
|
+
|
|
368
|
+
await expect(
|
|
369
|
+
verifyOTP(config, { code: '123456', otpId: 'otp-123' }),
|
|
370
|
+
).rejects.toThrow('Wallet not initialized')
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
describe('refreshSession', () => {
|
|
375
|
+
it('calls wallet.refreshSession with current session id', async () => {
|
|
376
|
+
const wallet = createMockWallet()
|
|
377
|
+
const currentSession = { id: 'current-session-123' }
|
|
378
|
+
const store = createMockStore(wallet, currentSession)
|
|
379
|
+
const connector = createMockConnector(store)
|
|
380
|
+
const config = createMockConfig(connector)
|
|
381
|
+
|
|
382
|
+
await refreshSession(config)
|
|
383
|
+
|
|
384
|
+
expect(wallet.refreshSession).toHaveBeenCalledWith('current-session-123')
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('updates session in store with new session and returns it', async () => {
|
|
388
|
+
const wallet = createMockWallet()
|
|
389
|
+
wallet.refreshSession.mockResolvedValue({ id: 'new-session-456' })
|
|
390
|
+
const currentSession = { id: 'current-session-123' }
|
|
391
|
+
const store = createMockStore(wallet, currentSession)
|
|
392
|
+
const connector = createMockConnector(store)
|
|
393
|
+
const config = createMockConfig(connector)
|
|
394
|
+
|
|
395
|
+
const result = await refreshSession(config)
|
|
396
|
+
|
|
397
|
+
expect(store.getState().setSession).toHaveBeenCalledWith({
|
|
398
|
+
id: 'new-session-456',
|
|
399
|
+
})
|
|
400
|
+
expect(result).toEqual({ id: 'new-session-456' })
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('sets session to null when refresh returns falsy', async () => {
|
|
404
|
+
const wallet = createMockWallet()
|
|
405
|
+
wallet.refreshSession.mockResolvedValue(null)
|
|
406
|
+
const currentSession = { id: 'current-session-123' }
|
|
407
|
+
const store = createMockStore(wallet, currentSession)
|
|
408
|
+
const connector = createMockConnector(store)
|
|
409
|
+
const config = createMockConfig(connector)
|
|
410
|
+
|
|
411
|
+
await refreshSession(config)
|
|
412
|
+
|
|
413
|
+
expect(store.getState().setSession).toHaveBeenCalledWith(null)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('throws when no active session', async () => {
|
|
417
|
+
const wallet = createMockWallet()
|
|
418
|
+
const store = createMockStore(wallet, null)
|
|
419
|
+
const connector = createMockConnector(store)
|
|
420
|
+
const config = createMockConfig(connector)
|
|
421
|
+
|
|
422
|
+
await expect(refreshSession(config)).rejects.toThrow(
|
|
423
|
+
'No active session to refresh',
|
|
424
|
+
)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it('throws when wallet is not initialized', async () => {
|
|
428
|
+
const store = createMockStore(null)
|
|
429
|
+
const connector = createMockConnector(store)
|
|
430
|
+
const config = createMockConfig(connector)
|
|
431
|
+
|
|
432
|
+
await expect(refreshSession(config)).rejects.toThrow(
|
|
433
|
+
'No active session to refresh',
|
|
434
|
+
)
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
describe('getUserEmail', () => {
|
|
439
|
+
it('calls wallet.client.getUserEmail with parameters', async () => {
|
|
440
|
+
const wallet = createMockWallet()
|
|
441
|
+
const session = {
|
|
442
|
+
id: 'session-123',
|
|
443
|
+
organizationId: 'org-123',
|
|
444
|
+
token: 'test-token',
|
|
445
|
+
}
|
|
446
|
+
const store = createMockStore(wallet, session)
|
|
447
|
+
const connector = createMockConnector(store)
|
|
448
|
+
const config = createMockConfig(connector)
|
|
449
|
+
|
|
450
|
+
await getUserEmail(config)
|
|
451
|
+
|
|
452
|
+
expect(wallet.client.getUserEmail).toHaveBeenCalledWith({
|
|
453
|
+
organizationId: 'org-123',
|
|
454
|
+
projectId: 'proj-123',
|
|
455
|
+
token: 'test-token',
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('returns email from wallet client', async () => {
|
|
460
|
+
const wallet = createMockWallet()
|
|
461
|
+
wallet.client.getUserEmail.mockResolvedValue({ email: 'test@test.com' })
|
|
462
|
+
const session = {
|
|
463
|
+
id: 'session-123',
|
|
464
|
+
organizationId: 'org-123',
|
|
465
|
+
token: 'test-token',
|
|
466
|
+
}
|
|
467
|
+
const store = createMockStore(wallet, session)
|
|
468
|
+
const connector = createMockConnector(store)
|
|
469
|
+
const config = createMockConfig(connector)
|
|
470
|
+
|
|
471
|
+
const result = await getUserEmail(config)
|
|
472
|
+
|
|
473
|
+
expect(result).toEqual({ email: 'test@test.com' })
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('throws when wallet is not initialized', async () => {
|
|
477
|
+
const store = createMockStore(null)
|
|
478
|
+
const connector = createMockConnector(store)
|
|
479
|
+
const config = createMockConfig(connector)
|
|
480
|
+
|
|
481
|
+
await expect(
|
|
482
|
+
getUserEmail(config, {
|
|
483
|
+
organizationId: 'org-123',
|
|
484
|
+
projectId: 'proj-456',
|
|
485
|
+
}),
|
|
486
|
+
).rejects.toThrow('Wallet not initialized')
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
describe('authenticateOAuth', () => {
|
|
491
|
+
let mockOpenOAuthPopup: ReturnType<typeof vi.fn>
|
|
492
|
+
let mockListenForOAuthMessage: ReturnType<typeof vi.fn>
|
|
493
|
+
let mockBuildBackendOAuthUrl: ReturnType<typeof vi.fn>
|
|
494
|
+
|
|
495
|
+
beforeEach(async () => {
|
|
496
|
+
const oauthModule = await import('./oauth.js')
|
|
497
|
+
mockOpenOAuthPopup = vi.mocked(oauthModule.openOAuthPopup)
|
|
498
|
+
mockListenForOAuthMessage = vi.mocked(oauthModule.listenForOAuthMessage)
|
|
499
|
+
mockBuildBackendOAuthUrl = vi.mocked(oauthModule.buildBackendOAuthUrl)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('throws when wallet is not initialized', async () => {
|
|
503
|
+
const store = createMockStore(null)
|
|
504
|
+
const connector = createMockConnector(store)
|
|
505
|
+
const config = createMockConfig(connector)
|
|
506
|
+
|
|
507
|
+
await expect(
|
|
508
|
+
authenticateOAuth(config, { provider: 'google' }),
|
|
509
|
+
).rejects.toThrow('Wallet not initialized')
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('throws when oauthConfig is missing', async () => {
|
|
513
|
+
const wallet = createMockWallet()
|
|
514
|
+
const store = createMockStore(wallet, null, null)
|
|
515
|
+
const connector = createMockConnector(store)
|
|
516
|
+
const config = createMockConfig(connector)
|
|
517
|
+
|
|
518
|
+
await expect(
|
|
519
|
+
authenticateOAuth(config, { provider: 'google' }),
|
|
520
|
+
).rejects.toThrow(
|
|
521
|
+
'Wallet not initialized. Please wait for connector setup.',
|
|
522
|
+
)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it('throws when publicKey is null', async () => {
|
|
526
|
+
const wallet = createMockWallet()
|
|
527
|
+
wallet.getPublicKey.mockResolvedValue(null)
|
|
528
|
+
const store = createMockStore(wallet)
|
|
529
|
+
const connector = createMockConnector(store)
|
|
530
|
+
const config = createMockConfig(connector)
|
|
531
|
+
|
|
532
|
+
await expect(
|
|
533
|
+
authenticateOAuth(config, { provider: 'google' }),
|
|
534
|
+
).rejects.toThrow('Failed to get wallet public key')
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it('throws when popup is blocked', async () => {
|
|
538
|
+
const wallet = createMockWallet()
|
|
539
|
+
const store = createMockStore(wallet)
|
|
540
|
+
const connector = createMockConnector(store)
|
|
541
|
+
const config = createMockConfig(connector)
|
|
542
|
+
|
|
543
|
+
mockOpenOAuthPopup.mockReturnValue(null)
|
|
544
|
+
|
|
545
|
+
await expect(
|
|
546
|
+
authenticateOAuth(config, { provider: 'google' }),
|
|
547
|
+
).rejects.toThrow('Failed to open google login window')
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('builds OAuth URL with correct parameters', async () => {
|
|
551
|
+
const wallet = createMockWallet()
|
|
552
|
+
wallet.getPublicKey.mockResolvedValue('03abcdef123456')
|
|
553
|
+
const store = createMockStore(wallet)
|
|
554
|
+
const connector = createMockConnector(store)
|
|
555
|
+
const config = createMockConfig(connector)
|
|
556
|
+
|
|
557
|
+
mockOpenOAuthPopup.mockReturnValue({ closed: false } as Window)
|
|
558
|
+
mockListenForOAuthMessage.mockImplementation(() => {
|
|
559
|
+
return () => {}
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
authenticateOAuth(config, { provider: 'google' }).catch(() => {})
|
|
563
|
+
|
|
564
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
565
|
+
|
|
566
|
+
expect(mockBuildBackendOAuthUrl).toHaveBeenCalledWith({
|
|
567
|
+
provider: 'google',
|
|
568
|
+
backendUrl: 'https://api.example.com',
|
|
569
|
+
projectId: 'proj-123',
|
|
570
|
+
publicKey: '03abcdef123456',
|
|
571
|
+
returnTo: expect.stringContaining('oauth_success=true'),
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
it('completes full OAuth success flow', async () => {
|
|
576
|
+
const wallet = createMockWallet()
|
|
577
|
+
wallet.getSession.mockResolvedValue({ id: 'oauth-session' })
|
|
578
|
+
wallet.toAccount.mockResolvedValue({ address: '0xoauth' })
|
|
579
|
+
const store = createMockStore(wallet)
|
|
580
|
+
const connector = createMockConnector(store)
|
|
581
|
+
const config = createMockConfig(connector)
|
|
582
|
+
|
|
583
|
+
mockOpenOAuthPopup.mockReturnValue({ closed: false } as Window)
|
|
584
|
+
// Simulate the success callback being called immediately
|
|
585
|
+
mockListenForOAuthMessage.mockImplementation(
|
|
586
|
+
(_win: Window, _origin: string, onSuccess: () => void) => {
|
|
587
|
+
// Call onSuccess asynchronously to simulate real behavior
|
|
588
|
+
setTimeout(() => onSuccess(), 0)
|
|
589
|
+
return () => {}
|
|
590
|
+
},
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
await authenticateOAuth(config, { provider: 'google' })
|
|
594
|
+
|
|
595
|
+
expect(wallet.auth).toHaveBeenCalledWith({
|
|
596
|
+
type: 'oauth',
|
|
597
|
+
provider: 'google',
|
|
598
|
+
})
|
|
599
|
+
expect(store.getState().setEoaAccount).toHaveBeenCalledWith({
|
|
600
|
+
address: '0xoauth',
|
|
601
|
+
})
|
|
602
|
+
expect(store.getState().setSession).toHaveBeenCalledWith({
|
|
603
|
+
id: 'oauth-session',
|
|
604
|
+
})
|
|
605
|
+
expect(wagmiConnect).toHaveBeenCalledWith(config, { connector })
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it('rejects when wallet.auth fails in success callback', async () => {
|
|
609
|
+
const wallet = createMockWallet()
|
|
610
|
+
wallet.auth.mockRejectedValue(new Error('Auth backend error'))
|
|
611
|
+
const store = createMockStore(wallet)
|
|
612
|
+
const connector = createMockConnector(store)
|
|
613
|
+
const config = createMockConfig(connector)
|
|
614
|
+
|
|
615
|
+
mockOpenOAuthPopup.mockReturnValue({ closed: false } as Window)
|
|
616
|
+
mockListenForOAuthMessage.mockImplementation(
|
|
617
|
+
(_win: Window, _origin: string, onSuccess: () => void) => {
|
|
618
|
+
setTimeout(() => onSuccess(), 0)
|
|
619
|
+
return () => {}
|
|
620
|
+
},
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
await expect(
|
|
624
|
+
authenticateOAuth(config, { provider: 'google' }),
|
|
625
|
+
).rejects.toThrow('Auth backend error')
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
it('rejects when onError callback is called', async () => {
|
|
629
|
+
const wallet = createMockWallet()
|
|
630
|
+
const store = createMockStore(wallet)
|
|
631
|
+
const connector = createMockConnector(store)
|
|
632
|
+
const config = createMockConfig(connector)
|
|
633
|
+
|
|
634
|
+
mockOpenOAuthPopup.mockReturnValue({ closed: false } as Window)
|
|
635
|
+
mockListenForOAuthMessage.mockImplementation(
|
|
636
|
+
(
|
|
637
|
+
_win: Window,
|
|
638
|
+
_origin: string,
|
|
639
|
+
_onSuccess: () => void,
|
|
640
|
+
onError: (error: Error) => void,
|
|
641
|
+
) => {
|
|
642
|
+
setTimeout(() => onError(new Error('Window closed')), 0)
|
|
643
|
+
return () => {}
|
|
644
|
+
},
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
await expect(
|
|
648
|
+
authenticateOAuth(config, { provider: 'google' }),
|
|
649
|
+
).rejects.toThrow('Window closed')
|
|
650
|
+
})
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
describe('exportWallet', () => {
|
|
654
|
+
it('throws when wallet is not initialized', async () => {
|
|
655
|
+
const store = createMockStore(null)
|
|
656
|
+
const connector = createMockConnector(store)
|
|
657
|
+
const config = createMockConfig(connector)
|
|
658
|
+
|
|
659
|
+
await expect(
|
|
660
|
+
exportWallet(config, { iframeContainerId: 'export-container' }),
|
|
661
|
+
).rejects.toThrow('Wallet not initialized')
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('throws when iframe container not found', async () => {
|
|
665
|
+
const wallet = createMockWallet()
|
|
666
|
+
const store = createMockStore(wallet)
|
|
667
|
+
const connector = createMockConnector(store)
|
|
668
|
+
const config = createMockConfig(connector)
|
|
669
|
+
|
|
670
|
+
await expect(
|
|
671
|
+
exportWallet(config, { iframeContainerId: 'nonexistent-container' }),
|
|
672
|
+
).rejects.toThrow('Iframe container not found')
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
it('completes full export wallet flow', async () => {
|
|
676
|
+
const wallet = createMockWallet()
|
|
677
|
+
const store = createMockStore(wallet)
|
|
678
|
+
const connector = createMockConnector(store)
|
|
679
|
+
const config = createMockConfig(connector)
|
|
680
|
+
|
|
681
|
+
// Create DOM element for iframe container
|
|
682
|
+
const container = document.createElement('div')
|
|
683
|
+
container.id = 'export-wallet-container'
|
|
684
|
+
document.body.appendChild(container)
|
|
685
|
+
|
|
686
|
+
const mockStamper = {
|
|
687
|
+
init: vi.fn().mockResolvedValue('target-public-key'),
|
|
688
|
+
injectWalletExportBundle: vi.fn().mockResolvedValue(true),
|
|
689
|
+
}
|
|
690
|
+
vi.mocked(createIframeStamper).mockResolvedValue(mockStamper as never)
|
|
691
|
+
vi.mocked(exportWalletSdk).mockResolvedValue({
|
|
692
|
+
exportBundle: 'wallet-bundle-data',
|
|
693
|
+
organizationId: 'org-abc',
|
|
694
|
+
} as never)
|
|
695
|
+
|
|
696
|
+
await exportWallet(config, {
|
|
697
|
+
iframeContainerId: 'export-wallet-container',
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
expect(createIframeStamper).toHaveBeenCalledWith({
|
|
701
|
+
iframeUrl: 'https://export.turnkey.com',
|
|
702
|
+
iframeContainer: container,
|
|
703
|
+
iframeElementId: 'export-wallet-iframe',
|
|
704
|
+
})
|
|
705
|
+
expect(mockStamper.init).toHaveBeenCalled()
|
|
706
|
+
expect(exportWalletSdk).toHaveBeenCalledWith({
|
|
707
|
+
wallet,
|
|
708
|
+
targetPublicKey: 'target-public-key',
|
|
709
|
+
})
|
|
710
|
+
expect(mockStamper.injectWalletExportBundle).toHaveBeenCalledWith(
|
|
711
|
+
'wallet-bundle-data',
|
|
712
|
+
'org-abc',
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
document.body.removeChild(container)
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
it('throws when inject bundle returns non-true', async () => {
|
|
719
|
+
const wallet = createMockWallet()
|
|
720
|
+
const store = createMockStore(wallet)
|
|
721
|
+
const connector = createMockConnector(store)
|
|
722
|
+
const config = createMockConfig(connector)
|
|
723
|
+
|
|
724
|
+
const container = document.createElement('div')
|
|
725
|
+
container.id = 'export-fail-container'
|
|
726
|
+
document.body.appendChild(container)
|
|
727
|
+
|
|
728
|
+
const mockStamper = {
|
|
729
|
+
init: vi.fn().mockResolvedValue('pub-key'),
|
|
730
|
+
injectWalletExportBundle: vi.fn().mockResolvedValue(false),
|
|
731
|
+
}
|
|
732
|
+
vi.mocked(createIframeStamper).mockResolvedValue(mockStamper as never)
|
|
733
|
+
vi.mocked(exportWalletSdk).mockResolvedValue({
|
|
734
|
+
exportBundle: 'bundle',
|
|
735
|
+
organizationId: 'org',
|
|
736
|
+
} as never)
|
|
737
|
+
|
|
738
|
+
await expect(
|
|
739
|
+
exportWallet(config, { iframeContainerId: 'export-fail-container' }),
|
|
740
|
+
).rejects.toThrow('Failed to inject export bundle')
|
|
741
|
+
|
|
742
|
+
document.body.removeChild(container)
|
|
743
|
+
})
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
describe('exportPrivateKey', () => {
|
|
747
|
+
it('throws when wallet is not initialized', async () => {
|
|
748
|
+
const store = createMockStore(null)
|
|
749
|
+
const connector = createMockConnector(store)
|
|
750
|
+
const config = createMockConfig(connector)
|
|
751
|
+
|
|
752
|
+
await expect(
|
|
753
|
+
exportPrivateKey(config, { iframeContainerId: 'export-container' }),
|
|
754
|
+
).rejects.toThrow('Wallet not initialized')
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it('throws when iframe container not found', async () => {
|
|
758
|
+
const wallet = createMockWallet()
|
|
759
|
+
const store = createMockStore(wallet)
|
|
760
|
+
const connector = createMockConnector(store)
|
|
761
|
+
const config = createMockConfig(connector)
|
|
762
|
+
|
|
763
|
+
await expect(
|
|
764
|
+
exportPrivateKey(config, {
|
|
765
|
+
iframeContainerId: 'nonexistent-container',
|
|
766
|
+
}),
|
|
767
|
+
).rejects.toThrow('Iframe container not found')
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
it('completes full export private key flow with defaults', async () => {
|
|
771
|
+
const wallet = createMockWallet()
|
|
772
|
+
const store = createMockStore(wallet)
|
|
773
|
+
const connector = createMockConnector(store)
|
|
774
|
+
const config = createMockConfig(connector)
|
|
775
|
+
|
|
776
|
+
const container = document.createElement('div')
|
|
777
|
+
container.id = 'export-pk-container'
|
|
778
|
+
document.body.appendChild(container)
|
|
779
|
+
|
|
780
|
+
const mockStamper = {
|
|
781
|
+
init: vi.fn().mockResolvedValue('target-pub-key'),
|
|
782
|
+
injectKeyExportBundle: vi.fn().mockResolvedValue(true),
|
|
783
|
+
}
|
|
784
|
+
vi.mocked(createIframeStamper).mockResolvedValue(mockStamper as never)
|
|
785
|
+
vi.mocked(exportPrivateKeySdk).mockResolvedValue({
|
|
786
|
+
exportBundle: 'pk-bundle-data',
|
|
787
|
+
organizationId: 'org-xyz',
|
|
788
|
+
} as never)
|
|
789
|
+
|
|
790
|
+
await exportPrivateKey(config, {
|
|
791
|
+
iframeContainerId: 'export-pk-container',
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
expect(createIframeStamper).toHaveBeenCalledWith({
|
|
795
|
+
iframeUrl: 'https://export.turnkey.com',
|
|
796
|
+
iframeContainer: container,
|
|
797
|
+
iframeElementId: 'export-private-key-iframe',
|
|
798
|
+
})
|
|
799
|
+
expect(exportPrivateKeySdk).toHaveBeenCalledWith({
|
|
800
|
+
wallet,
|
|
801
|
+
targetPublicKey: 'target-pub-key',
|
|
802
|
+
})
|
|
803
|
+
// Default keyFormat is 'Hexadecimal'
|
|
804
|
+
expect(mockStamper.injectKeyExportBundle).toHaveBeenCalledWith(
|
|
805
|
+
'pk-bundle-data',
|
|
806
|
+
'org-xyz',
|
|
807
|
+
'Hexadecimal',
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
document.body.removeChild(container)
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
it('passes address and custom keyFormat when provided', async () => {
|
|
814
|
+
const wallet = createMockWallet()
|
|
815
|
+
const store = createMockStore(wallet)
|
|
816
|
+
const connector = createMockConnector(store)
|
|
817
|
+
const config = createMockConfig(connector)
|
|
818
|
+
|
|
819
|
+
const container = document.createElement('div')
|
|
820
|
+
container.id = 'export-pk-custom'
|
|
821
|
+
document.body.appendChild(container)
|
|
822
|
+
|
|
823
|
+
const mockStamper = {
|
|
824
|
+
init: vi.fn().mockResolvedValue('pub'),
|
|
825
|
+
injectKeyExportBundle: vi.fn().mockResolvedValue(true),
|
|
826
|
+
}
|
|
827
|
+
vi.mocked(createIframeStamper).mockResolvedValue(mockStamper as never)
|
|
828
|
+
vi.mocked(exportPrivateKeySdk).mockResolvedValue({
|
|
829
|
+
exportBundle: 'bundle',
|
|
830
|
+
organizationId: 'org',
|
|
831
|
+
} as never)
|
|
832
|
+
|
|
833
|
+
await exportPrivateKey(config, {
|
|
834
|
+
iframeContainerId: 'export-pk-custom',
|
|
835
|
+
address: '0xmyaddress',
|
|
836
|
+
keyFormat: 'Solana',
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
expect(exportPrivateKeySdk).toHaveBeenCalledWith({
|
|
840
|
+
wallet,
|
|
841
|
+
targetPublicKey: 'pub',
|
|
842
|
+
address: '0xmyaddress',
|
|
843
|
+
})
|
|
844
|
+
expect(mockStamper.injectKeyExportBundle).toHaveBeenCalledWith(
|
|
845
|
+
'bundle',
|
|
846
|
+
'org',
|
|
847
|
+
'Solana',
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
document.body.removeChild(container)
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
it('throws when inject key bundle returns non-true', async () => {
|
|
854
|
+
const wallet = createMockWallet()
|
|
855
|
+
const store = createMockStore(wallet)
|
|
856
|
+
const connector = createMockConnector(store)
|
|
857
|
+
const config = createMockConfig(connector)
|
|
858
|
+
|
|
859
|
+
const container = document.createElement('div')
|
|
860
|
+
container.id = 'export-pk-fail'
|
|
861
|
+
document.body.appendChild(container)
|
|
862
|
+
|
|
863
|
+
const mockStamper = {
|
|
864
|
+
init: vi.fn().mockResolvedValue('pub'),
|
|
865
|
+
injectKeyExportBundle: vi.fn().mockResolvedValue(false),
|
|
866
|
+
}
|
|
867
|
+
vi.mocked(createIframeStamper).mockResolvedValue(mockStamper as never)
|
|
868
|
+
vi.mocked(exportPrivateKeySdk).mockResolvedValue({
|
|
869
|
+
exportBundle: 'bundle',
|
|
870
|
+
organizationId: 'org',
|
|
871
|
+
} as never)
|
|
872
|
+
|
|
873
|
+
await expect(
|
|
874
|
+
exportPrivateKey(config, { iframeContainerId: 'export-pk-fail' }),
|
|
875
|
+
).rejects.toThrow('Failed to inject export bundle')
|
|
876
|
+
|
|
877
|
+
document.body.removeChild(container)
|
|
878
|
+
})
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
describe('connector lookup', () => {
|
|
882
|
+
it('throws when ZeroDev connector not found in config', async () => {
|
|
883
|
+
const config = {
|
|
884
|
+
connectors: [{ id: 'other-connector' }],
|
|
885
|
+
chains: [],
|
|
886
|
+
state: { chainId: 1, connections: new Map() },
|
|
887
|
+
storage: null,
|
|
888
|
+
} as unknown as Config
|
|
889
|
+
|
|
890
|
+
await expect(
|
|
891
|
+
registerPasskey(config, { email: 'user@example.com' }),
|
|
892
|
+
).rejects.toThrow('ZeroDev connector not found in Wagmi config')
|
|
893
|
+
})
|
|
894
|
+
})
|
|
895
|
+
})
|