@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,248 @@
|
|
|
1
|
+
import { createConnector } from '@wagmi/core';
|
|
2
|
+
import { createKernelAccount, createKernelAccountClient, createZeroDevPaymasterClient, } from '@zerodev/sdk';
|
|
3
|
+
import { getEntryPoint, KERNEL_V3_3 } from '@zerodev/sdk/constants';
|
|
4
|
+
import { createZeroDevWallet } from '@zerodev/wallet-core';
|
|
5
|
+
import { createPublicClient, http } from 'viem';
|
|
6
|
+
import { createProvider } from './provider.js';
|
|
7
|
+
import { createZeroDevWalletStore } from './store.js';
|
|
8
|
+
import { getAAUrl } from './utils/aaUtils.js';
|
|
9
|
+
export function zeroDevWallet(params) {
|
|
10
|
+
return createConnector((wagmiConfig) => {
|
|
11
|
+
let store;
|
|
12
|
+
let provider;
|
|
13
|
+
let initPromise;
|
|
14
|
+
// Get transports from Wagmi config (uses user's RPC URLs)
|
|
15
|
+
const transports = wagmiConfig.transports;
|
|
16
|
+
// Lazy initialization - only runs on client side
|
|
17
|
+
const initialize = async () => {
|
|
18
|
+
initPromise ??= (async () => {
|
|
19
|
+
console.log('Initializing ZeroDevWallet connector...');
|
|
20
|
+
// Initialize wallet SDK
|
|
21
|
+
const wallet = await createZeroDevWallet({
|
|
22
|
+
projectId: params.projectId,
|
|
23
|
+
...(params.organizationId && {
|
|
24
|
+
organizationId: params.organizationId,
|
|
25
|
+
}),
|
|
26
|
+
...(params.proxyBaseUrl && { proxyBaseUrl: params.proxyBaseUrl }),
|
|
27
|
+
...(params.sessionStorage && {
|
|
28
|
+
sessionStorage: params.sessionStorage,
|
|
29
|
+
}),
|
|
30
|
+
...(params.rpId && { rpId: params.rpId }),
|
|
31
|
+
});
|
|
32
|
+
// Create store
|
|
33
|
+
store = createZeroDevWalletStore();
|
|
34
|
+
store.getState().setWallet(wallet);
|
|
35
|
+
// Initialize chainIds
|
|
36
|
+
const chainIds = params.chains.map((c) => c.id);
|
|
37
|
+
store.setState({ chainIds });
|
|
38
|
+
// Store OAuth config if provided
|
|
39
|
+
if (params.oauthConfig) {
|
|
40
|
+
store.getState().setOAuthConfig(params.oauthConfig);
|
|
41
|
+
}
|
|
42
|
+
// Create EIP-1193 provider
|
|
43
|
+
provider = createProvider({
|
|
44
|
+
store,
|
|
45
|
+
config: params,
|
|
46
|
+
chains: Array.from(params.chains),
|
|
47
|
+
});
|
|
48
|
+
// Check for existing session (page reload)
|
|
49
|
+
const session = await wallet.getSession();
|
|
50
|
+
if (session) {
|
|
51
|
+
console.log('Found existing session, restoring...');
|
|
52
|
+
const eoaAccount = await wallet.toAccount();
|
|
53
|
+
store.getState().setEoaAccount(eoaAccount);
|
|
54
|
+
store.getState().setSession(session);
|
|
55
|
+
}
|
|
56
|
+
console.log('ZeroDevWallet connector initialized');
|
|
57
|
+
})();
|
|
58
|
+
return initPromise;
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
id: 'zerodev-wallet',
|
|
62
|
+
name: 'ZeroDevWallet',
|
|
63
|
+
type: 'injected',
|
|
64
|
+
async setup() {
|
|
65
|
+
// Initialize on client-side mount (setup only runs client-side)
|
|
66
|
+
if (typeof window !== 'undefined') {
|
|
67
|
+
await initialize();
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
async connect({ chainId, ...rest } = {}) {
|
|
71
|
+
const withCapabilities = ('withCapabilities' in rest && rest.withCapabilities) || false;
|
|
72
|
+
const isReconnecting = ('isReconnecting' in rest && rest.isReconnecting) || false;
|
|
73
|
+
// Ensure wallet is initialized (lazy init on first connect)
|
|
74
|
+
await initialize();
|
|
75
|
+
console.log(isReconnecting
|
|
76
|
+
? 'Reconnecting ZeroDevWallet...'
|
|
77
|
+
: 'Connecting ZeroDevWallet...');
|
|
78
|
+
const state = store.getState();
|
|
79
|
+
// Determine active chain
|
|
80
|
+
const activeChainId = chainId || state.chainIds[0];
|
|
81
|
+
if (!activeChainId) {
|
|
82
|
+
throw new Error('No chain configured');
|
|
83
|
+
}
|
|
84
|
+
// If reconnecting and already have kernel account, return immediately
|
|
85
|
+
if (isReconnecting && state.kernelAccounts.has(activeChainId)) {
|
|
86
|
+
const kernelAccount = state.kernelAccounts.get(activeChainId);
|
|
87
|
+
if (kernelAccount?.address) {
|
|
88
|
+
console.log('Already connected:', kernelAccount.address);
|
|
89
|
+
return {
|
|
90
|
+
accounts: [kernelAccount.address],
|
|
91
|
+
chainId: activeChainId,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!state.eoaAccount) {
|
|
96
|
+
throw new Error('Not authenticated. Please authenticate first using passkey, OAuth, or OTP.');
|
|
97
|
+
}
|
|
98
|
+
// Create KernelAccount for this chain if doesn't exist
|
|
99
|
+
if (!state.kernelAccounts.has(activeChainId)) {
|
|
100
|
+
const chain = params.chains.find((c) => c.id === activeChainId);
|
|
101
|
+
if (!chain) {
|
|
102
|
+
throw new Error(`Chain ${activeChainId} not found in config`);
|
|
103
|
+
}
|
|
104
|
+
// Use transport from Wagmi config (has user's RPC URL)
|
|
105
|
+
const transport = transports?.[activeChainId] ?? http();
|
|
106
|
+
const publicClient = createPublicClient({
|
|
107
|
+
chain,
|
|
108
|
+
transport,
|
|
109
|
+
});
|
|
110
|
+
console.log(`Creating kernel account for chain ${activeChainId}...`);
|
|
111
|
+
const kernelAccount = await createKernelAccount(publicClient, {
|
|
112
|
+
entryPoint: getEntryPoint('0.7'),
|
|
113
|
+
kernelVersion: KERNEL_V3_3,
|
|
114
|
+
eip7702Account: state.eoaAccount,
|
|
115
|
+
});
|
|
116
|
+
// Store kernel account for this chain
|
|
117
|
+
store.getState().setKernelAccount(activeChainId, kernelAccount);
|
|
118
|
+
// Create and store kernel client for transactions
|
|
119
|
+
const kernelClient = createKernelAccountClient({
|
|
120
|
+
account: kernelAccount,
|
|
121
|
+
bundlerTransport: http(getAAUrl(activeChainId, params.aaUrl)),
|
|
122
|
+
chain,
|
|
123
|
+
client: publicClient,
|
|
124
|
+
paymaster: createZeroDevPaymasterClient({
|
|
125
|
+
chain,
|
|
126
|
+
transport: http(getAAUrl(activeChainId, params.aaUrl)),
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
store.getState().setKernelClient(activeChainId, kernelClient);
|
|
130
|
+
}
|
|
131
|
+
// Set as active chain
|
|
132
|
+
store.getState().setActiveChain(activeChainId);
|
|
133
|
+
// Get fresh state after updates
|
|
134
|
+
const freshState = store.getState();
|
|
135
|
+
const kernelAccount = freshState.kernelAccounts.get(activeChainId);
|
|
136
|
+
console.log('ZeroDevWallet connected:', kernelAccount.address);
|
|
137
|
+
const address = kernelAccount.address;
|
|
138
|
+
return {
|
|
139
|
+
accounts: (withCapabilities
|
|
140
|
+
? [{ address, capabilities: {} }]
|
|
141
|
+
: [address]),
|
|
142
|
+
chainId: activeChainId,
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
async disconnect() {
|
|
146
|
+
console.log('Disconnecting ZeroDevWallet...');
|
|
147
|
+
if (!store)
|
|
148
|
+
return;
|
|
149
|
+
const wallet = store.getState().wallet;
|
|
150
|
+
// Cleanup provider (clears timers)
|
|
151
|
+
provider?.destroy();
|
|
152
|
+
await wallet?.logout();
|
|
153
|
+
store.getState().clear();
|
|
154
|
+
},
|
|
155
|
+
async getAccounts() {
|
|
156
|
+
if (!store)
|
|
157
|
+
return [];
|
|
158
|
+
const { eoaAccount, kernelAccounts, chainIds } = store.getState();
|
|
159
|
+
// Return EOA address if we have it (EIP-7702: EOA address = kernel address)
|
|
160
|
+
if (eoaAccount) {
|
|
161
|
+
return [eoaAccount.address];
|
|
162
|
+
}
|
|
163
|
+
// Fallback: check kernel accounts
|
|
164
|
+
const activeAccount = chainIds[0]
|
|
165
|
+
? kernelAccounts.get(chainIds[0])
|
|
166
|
+
: null;
|
|
167
|
+
return activeAccount ? [activeAccount.address] : [];
|
|
168
|
+
},
|
|
169
|
+
async getChainId() {
|
|
170
|
+
if (!store)
|
|
171
|
+
return params.chains[0].id;
|
|
172
|
+
return store.getState().chainIds[0] || params.chains[0].id;
|
|
173
|
+
},
|
|
174
|
+
async getProvider() {
|
|
175
|
+
await initialize();
|
|
176
|
+
return provider;
|
|
177
|
+
},
|
|
178
|
+
async switchChain({ chainId }) {
|
|
179
|
+
await initialize();
|
|
180
|
+
console.log(`Switching to chain ${chainId}...`);
|
|
181
|
+
const state = store.getState();
|
|
182
|
+
if (!state.eoaAccount) {
|
|
183
|
+
throw new Error('Not authenticated');
|
|
184
|
+
}
|
|
185
|
+
// Update active chain
|
|
186
|
+
store.getState().setActiveChain(chainId);
|
|
187
|
+
// Create kernel account for new chain if doesn't exist
|
|
188
|
+
if (!state.kernelAccounts.has(chainId)) {
|
|
189
|
+
const chain = params.chains.find((c) => c.id === chainId);
|
|
190
|
+
if (!chain) {
|
|
191
|
+
throw new Error(`Chain ${chainId} not found in config`);
|
|
192
|
+
}
|
|
193
|
+
// Use transport from Wagmi config (has user's RPC URL)
|
|
194
|
+
const transport = transports?.[chainId] ?? http();
|
|
195
|
+
const publicClient = createPublicClient({
|
|
196
|
+
chain,
|
|
197
|
+
transport,
|
|
198
|
+
});
|
|
199
|
+
console.log(`Creating kernel account for chain ${chainId}...`);
|
|
200
|
+
const kernelAccount = await createKernelAccount(publicClient, {
|
|
201
|
+
entryPoint: getEntryPoint('0.7'),
|
|
202
|
+
kernelVersion: KERNEL_V3_3,
|
|
203
|
+
eip7702Account: state.eoaAccount,
|
|
204
|
+
});
|
|
205
|
+
store.getState().setKernelAccount(chainId, kernelAccount);
|
|
206
|
+
const kernelClient = createKernelAccountClient({
|
|
207
|
+
account: kernelAccount,
|
|
208
|
+
bundlerTransport: http(getAAUrl(chainId, params.aaUrl)),
|
|
209
|
+
chain,
|
|
210
|
+
client: publicClient,
|
|
211
|
+
paymaster: createZeroDevPaymasterClient({
|
|
212
|
+
chain,
|
|
213
|
+
transport: http(getAAUrl(chainId, params.aaUrl)),
|
|
214
|
+
}),
|
|
215
|
+
});
|
|
216
|
+
store.getState().setKernelClient(chainId, kernelClient);
|
|
217
|
+
}
|
|
218
|
+
return params.chains.find((c) => c.id === chainId);
|
|
219
|
+
},
|
|
220
|
+
async isAuthorized() {
|
|
221
|
+
// Just check if we have a session - don't initialize here (too slow)
|
|
222
|
+
if (!store)
|
|
223
|
+
return false;
|
|
224
|
+
return !!store.getState().eoaAccount;
|
|
225
|
+
},
|
|
226
|
+
// Custom method for hooks to access store
|
|
227
|
+
async getStore() {
|
|
228
|
+
await initialize();
|
|
229
|
+
return store;
|
|
230
|
+
},
|
|
231
|
+
// Event listeners
|
|
232
|
+
onAccountsChanged() {
|
|
233
|
+
// Not applicable for this wallet type
|
|
234
|
+
},
|
|
235
|
+
onChainChanged() {
|
|
236
|
+
// Handled by Wagmi
|
|
237
|
+
},
|
|
238
|
+
onConnect() {
|
|
239
|
+
// Handled by Wagmi
|
|
240
|
+
},
|
|
241
|
+
onDisconnect() {
|
|
242
|
+
console.log('Disconnect event');
|
|
243
|
+
provider?.destroy();
|
|
244
|
+
store.getState().clear();
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ZERODEV_AA_URL = 'https://rpc.zerodev.app/api/v3/';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMutation, } from '@tanstack/react-query';
|
|
3
|
+
import { useConfig } from 'wagmi';
|
|
4
|
+
import { authenticateOAuth } from '../actions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Hook to authenticate with OAuth (opens popup)
|
|
7
|
+
*/
|
|
8
|
+
export function useAuthenticateOAuth(parameters = {}) {
|
|
9
|
+
const { mutation } = parameters;
|
|
10
|
+
const config = useConfig(parameters);
|
|
11
|
+
return useMutation({
|
|
12
|
+
...mutation,
|
|
13
|
+
async mutationFn(variables) {
|
|
14
|
+
return authenticateOAuth(config, variables);
|
|
15
|
+
},
|
|
16
|
+
mutationKey: ['authenticateOAuth'],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMutation, } from '@tanstack/react-query';
|
|
3
|
+
import { useConfig } from 'wagmi';
|
|
4
|
+
import { exportWallet } from '../actions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Hook to export wallet seed phrase
|
|
7
|
+
*/
|
|
8
|
+
export function useExportWallet(parameters = {}) {
|
|
9
|
+
const { mutation } = parameters;
|
|
10
|
+
const config = useConfig(parameters);
|
|
11
|
+
return useMutation({
|
|
12
|
+
...mutation,
|
|
13
|
+
async mutationFn(variables) {
|
|
14
|
+
return exportWallet(config, variables);
|
|
15
|
+
},
|
|
16
|
+
mutationKey: ['exportWallet'],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMutation, } from '@tanstack/react-query';
|
|
3
|
+
import { useConfig } from 'wagmi';
|
|
4
|
+
import { loginPasskey } from '../actions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Hook to login with passkey
|
|
7
|
+
*/
|
|
8
|
+
export function useLoginPasskey(parameters = {}) {
|
|
9
|
+
const { mutation } = parameters;
|
|
10
|
+
const config = useConfig(parameters);
|
|
11
|
+
return useMutation({
|
|
12
|
+
...mutation,
|
|
13
|
+
async mutationFn(variables) {
|
|
14
|
+
return loginPasskey(config, variables);
|
|
15
|
+
},
|
|
16
|
+
mutationKey: ['loginPasskey'],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMutation, } from '@tanstack/react-query';
|
|
3
|
+
import { useConfig } from 'wagmi';
|
|
4
|
+
import { refreshSession } from '../actions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Hook to manually refresh session
|
|
7
|
+
*/
|
|
8
|
+
export function useRefreshSession(parameters = {}) {
|
|
9
|
+
const { mutation } = parameters;
|
|
10
|
+
const config = useConfig(parameters);
|
|
11
|
+
return useMutation({
|
|
12
|
+
...mutation,
|
|
13
|
+
async mutationFn(variables) {
|
|
14
|
+
return refreshSession(config, variables);
|
|
15
|
+
},
|
|
16
|
+
mutationKey: ['refreshSession'],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMutation, } from '@tanstack/react-query';
|
|
3
|
+
import { useConfig } from 'wagmi';
|
|
4
|
+
import { registerPasskey } from '../actions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Hook to register with passkey
|
|
7
|
+
*/
|
|
8
|
+
export function useRegisterPasskey(parameters = {}) {
|
|
9
|
+
const { mutation } = parameters;
|
|
10
|
+
const config = useConfig(parameters);
|
|
11
|
+
return useMutation({
|
|
12
|
+
...mutation,
|
|
13
|
+
async mutationFn(variables) {
|
|
14
|
+
return registerPasskey(config, variables);
|
|
15
|
+
},
|
|
16
|
+
mutationKey: ['registerPasskey'],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMutation, } from '@tanstack/react-query';
|
|
3
|
+
import { useConfig } from 'wagmi';
|
|
4
|
+
import { sendOTP } from '../actions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Hook to send OTP via email
|
|
7
|
+
*/
|
|
8
|
+
export function useSendOTP(parameters = {}) {
|
|
9
|
+
const { mutation } = parameters;
|
|
10
|
+
const config = useConfig(parameters);
|
|
11
|
+
return useMutation({
|
|
12
|
+
...mutation,
|
|
13
|
+
async mutationFn(variables) {
|
|
14
|
+
return sendOTP(config, variables);
|
|
15
|
+
},
|
|
16
|
+
mutationKey: ['sendOTP'],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMutation, } from '@tanstack/react-query';
|
|
3
|
+
import { useConfig } from 'wagmi';
|
|
4
|
+
import { verifyOTP } from '../actions.js';
|
|
5
|
+
/**
|
|
6
|
+
* Hook to verify OTP code
|
|
7
|
+
*/
|
|
8
|
+
export function useVerifyOTP(parameters = {}) {
|
|
9
|
+
const { mutation } = parameters;
|
|
10
|
+
const config = useConfig(parameters);
|
|
11
|
+
return useMutation({
|
|
12
|
+
...mutation,
|
|
13
|
+
async mutationFn(variables) {
|
|
14
|
+
return verifyOTP(config, variables);
|
|
15
|
+
},
|
|
16
|
+
mutationKey: ['verifyOTP'],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { zeroDevWallet } from './connector.js';
|
|
2
|
+
export { useAuthenticateOAuth } from './hooks/useAuthenticateOAuth.js';
|
|
3
|
+
export { useExportWallet } from './hooks/useExportWallet.js';
|
|
4
|
+
export { useLoginPasskey } from './hooks/useLoginPasskey.js';
|
|
5
|
+
export { useRefreshSession } from './hooks/useRefreshSession.js';
|
|
6
|
+
export { useRegisterPasskey } from './hooks/useRegisterPasskey.js';
|
|
7
|
+
export { useSendOTP } from './hooks/useSendOTP.js';
|
|
8
|
+
export { useVerifyOTP } from './hooks/useVerifyOTP.js';
|
|
9
|
+
export { OAUTH_PROVIDERS } from './oauth.js';
|
|
10
|
+
export { createZeroDevWalletStore } from './store.js';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { sha256 } from 'viem';
|
|
2
|
+
export const OAUTH_PROVIDERS = {
|
|
3
|
+
GOOGLE: 'google',
|
|
4
|
+
};
|
|
5
|
+
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
6
|
+
const POPUP_WIDTH = 500;
|
|
7
|
+
const POPUP_HEIGHT = 600;
|
|
8
|
+
export function buildOAuthUrl(params) {
|
|
9
|
+
const { provider, clientId, redirectUri, nonce, state } = params;
|
|
10
|
+
if (provider !== OAUTH_PROVIDERS.GOOGLE) {
|
|
11
|
+
throw new Error(`Unsupported OAuth provider: ${provider}`);
|
|
12
|
+
}
|
|
13
|
+
const authUrl = new URL(GOOGLE_AUTH_URL);
|
|
14
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
15
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
16
|
+
authUrl.searchParams.set('response_type', 'id_token');
|
|
17
|
+
authUrl.searchParams.set('scope', 'openid email profile');
|
|
18
|
+
authUrl.searchParams.set('nonce', nonce);
|
|
19
|
+
authUrl.searchParams.set('prompt', 'select_account');
|
|
20
|
+
let stateParam = `provider=${provider}`;
|
|
21
|
+
if (state) {
|
|
22
|
+
const additionalState = Object.entries(state)
|
|
23
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
24
|
+
.join('&');
|
|
25
|
+
if (additionalState) {
|
|
26
|
+
stateParam += `&${additionalState}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
authUrl.searchParams.set('state', stateParam);
|
|
30
|
+
return authUrl.toString();
|
|
31
|
+
}
|
|
32
|
+
export function openOAuthPopup(url) {
|
|
33
|
+
const width = POPUP_WIDTH;
|
|
34
|
+
const height = POPUP_HEIGHT;
|
|
35
|
+
const left = window.screenX + (window.innerWidth - width) / 2;
|
|
36
|
+
const top = window.screenY + (window.innerHeight - height) / 2;
|
|
37
|
+
const authWindow = window.open('about:blank', '_blank', `width=${width},height=${height},top=${top},left=${left},scrollbars=yes,resizable=yes`);
|
|
38
|
+
if (authWindow) {
|
|
39
|
+
authWindow.location.href = url;
|
|
40
|
+
}
|
|
41
|
+
return authWindow;
|
|
42
|
+
}
|
|
43
|
+
export function extractOAuthToken(url) {
|
|
44
|
+
const hashParams = new URLSearchParams(url.split('#')[1]);
|
|
45
|
+
let idToken = hashParams.get('id_token');
|
|
46
|
+
if (!idToken) {
|
|
47
|
+
const queryParams = new URLSearchParams(url.split('?')[1]);
|
|
48
|
+
idToken = queryParams.get('id_token');
|
|
49
|
+
}
|
|
50
|
+
return idToken;
|
|
51
|
+
}
|
|
52
|
+
export function pollOAuthPopup(authWindow, originUrl, onSuccess, onError) {
|
|
53
|
+
const interval = setInterval(() => {
|
|
54
|
+
try {
|
|
55
|
+
if (authWindow.closed) {
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
onError(new Error('Authentication window was closed'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const url = authWindow.location.href || '';
|
|
61
|
+
if (url.startsWith(originUrl)) {
|
|
62
|
+
const token = extractOAuthToken(url);
|
|
63
|
+
if (token) {
|
|
64
|
+
authWindow.close();
|
|
65
|
+
clearInterval(interval);
|
|
66
|
+
onSuccess(token);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Ignore cross-origin errors
|
|
72
|
+
}
|
|
73
|
+
}, 500);
|
|
74
|
+
}
|
|
75
|
+
export function generateOAuthNonce(publicKey) {
|
|
76
|
+
return sha256(publicKey).replace(/^0x/, '');
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type": "module","sideEffects":false}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { normalizeTimestamp } from '@zerodev/wallet-core';
|
|
2
|
+
import { Provider } from 'ox';
|
|
3
|
+
const SESSION_WARNING_THRESHOLD_MS = 60 * 1000; // 1 minute before expiry
|
|
4
|
+
export function createProvider({ store, config, }) {
|
|
5
|
+
const emitter = Provider.createEmitter();
|
|
6
|
+
let sessionRefreshTimer = null;
|
|
7
|
+
// Session auto-refresh logic
|
|
8
|
+
const scheduleSessionRefresh = () => {
|
|
9
|
+
if (config.autoRefreshSession === false)
|
|
10
|
+
return;
|
|
11
|
+
const state = store.getState();
|
|
12
|
+
if (!state.session || !state.wallet)
|
|
13
|
+
return;
|
|
14
|
+
const expiryMs = normalizeTimestamp(state.session.expiry);
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const timeUntilExpiry = expiryMs - now;
|
|
17
|
+
if (timeUntilExpiry <= 0) {
|
|
18
|
+
console.log('Session already expired');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Clear existing timer
|
|
22
|
+
if (sessionRefreshTimer) {
|
|
23
|
+
clearTimeout(sessionRefreshTimer);
|
|
24
|
+
sessionRefreshTimer = null;
|
|
25
|
+
}
|
|
26
|
+
const threshold = config.sessionWarningThreshold || SESSION_WARNING_THRESHOLD_MS;
|
|
27
|
+
const refreshAt = expiryMs - threshold;
|
|
28
|
+
const timeUntilRefresh = refreshAt - now;
|
|
29
|
+
if (timeUntilRefresh <= 0) {
|
|
30
|
+
console.log('Session expiring soon, refreshing immediately...');
|
|
31
|
+
refreshSessionNow();
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(`Scheduling session refresh in ${timeUntilRefresh}ms`);
|
|
35
|
+
sessionRefreshTimer = setTimeout(() => {
|
|
36
|
+
refreshSessionNow();
|
|
37
|
+
}, timeUntilRefresh);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const refreshSessionNow = async () => {
|
|
41
|
+
const state = store.getState();
|
|
42
|
+
if (!state.wallet || !state.session)
|
|
43
|
+
return;
|
|
44
|
+
console.log('Auto-refreshing session...');
|
|
45
|
+
store.getState().setIsExpiring(true);
|
|
46
|
+
try {
|
|
47
|
+
const newSession = await state.wallet.refreshSession(state.session.id);
|
|
48
|
+
console.log('Session refreshed successfully');
|
|
49
|
+
store.getState().setSession(newSession || null);
|
|
50
|
+
store.getState().setIsExpiring(false);
|
|
51
|
+
if (newSession) {
|
|
52
|
+
scheduleSessionRefresh();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.error('Session refresh failed:', err);
|
|
57
|
+
store.getState().setIsExpiring(false);
|
|
58
|
+
store.getState().clear();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
// Subscribe to session changes
|
|
62
|
+
const unsubscribe = store.subscribe((state) => state.session, () => {
|
|
63
|
+
scheduleSessionRefresh();
|
|
64
|
+
});
|
|
65
|
+
// Schedule initial refresh if session exists
|
|
66
|
+
const initialSession = store.getState().session;
|
|
67
|
+
if (initialSession) {
|
|
68
|
+
scheduleSessionRefresh();
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...emitter,
|
|
72
|
+
destroy() {
|
|
73
|
+
// Cleanup timer and subscription
|
|
74
|
+
if (sessionRefreshTimer) {
|
|
75
|
+
clearTimeout(sessionRefreshTimer);
|
|
76
|
+
sessionRefreshTimer = null;
|
|
77
|
+
}
|
|
78
|
+
unsubscribe();
|
|
79
|
+
},
|
|
80
|
+
async request({ method, params }) {
|
|
81
|
+
const state = store.getState();
|
|
82
|
+
const activeChainId = state.chainIds[0];
|
|
83
|
+
switch (method) {
|
|
84
|
+
case 'eth_accounts': {
|
|
85
|
+
const account = state.kernelAccounts.get(activeChainId);
|
|
86
|
+
return account ? [account.address] : [];
|
|
87
|
+
}
|
|
88
|
+
case 'eth_requestAccounts': {
|
|
89
|
+
const account = state.kernelAccounts.get(activeChainId);
|
|
90
|
+
if (!account)
|
|
91
|
+
throw new Error('Not authenticated');
|
|
92
|
+
return [account.address];
|
|
93
|
+
}
|
|
94
|
+
case 'eth_chainId': {
|
|
95
|
+
return `0x${activeChainId.toString(16)}`;
|
|
96
|
+
}
|
|
97
|
+
case 'wallet_sendTransaction':
|
|
98
|
+
case 'eth_sendTransaction': {
|
|
99
|
+
if (!params || params.length === 0) {
|
|
100
|
+
throw new Error('Missing transaction parameters');
|
|
101
|
+
}
|
|
102
|
+
const [tx] = params;
|
|
103
|
+
const chainId = tx.chainId ? parseInt(tx.chainId, 16) : activeChainId;
|
|
104
|
+
// Get kernel client for this chain
|
|
105
|
+
const kernelClient = store.getState().kernelClients.get(chainId);
|
|
106
|
+
if (!kernelClient) {
|
|
107
|
+
throw new Error(`No kernel client for chain ${chainId}`);
|
|
108
|
+
}
|
|
109
|
+
// Send gasless transaction (always UserOp for EIP-7702)
|
|
110
|
+
const hash = await kernelClient.sendTransaction({
|
|
111
|
+
calls: [
|
|
112
|
+
{
|
|
113
|
+
to: tx.to,
|
|
114
|
+
value: tx.value ? BigInt(tx.value) : 0n,
|
|
115
|
+
data: tx.data || '0x',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
return hash;
|
|
120
|
+
}
|
|
121
|
+
case 'personal_sign': {
|
|
122
|
+
if (!params || params.length < 2) {
|
|
123
|
+
throw new Error('Missing sign parameters');
|
|
124
|
+
}
|
|
125
|
+
const [message] = params;
|
|
126
|
+
let account = state.kernelAccounts.get(activeChainId);
|
|
127
|
+
if (account &&
|
|
128
|
+
'isDeployed' in account &&
|
|
129
|
+
!(await account.isDeployed())) {
|
|
130
|
+
account = state.eoaAccount;
|
|
131
|
+
}
|
|
132
|
+
if (!account)
|
|
133
|
+
throw new Error('Not authenticated');
|
|
134
|
+
return await account.signMessage({ message });
|
|
135
|
+
}
|
|
136
|
+
case 'eth_signTypedData_v4': {
|
|
137
|
+
if (!params || params.length < 2) {
|
|
138
|
+
throw new Error('Missing typed data parameters');
|
|
139
|
+
}
|
|
140
|
+
const [, typedDataJson] = params;
|
|
141
|
+
let account = state.kernelAccounts.get(activeChainId);
|
|
142
|
+
if (account &&
|
|
143
|
+
'isDeployed' in account &&
|
|
144
|
+
!(await account.isDeployed())) {
|
|
145
|
+
account = state.eoaAccount;
|
|
146
|
+
}
|
|
147
|
+
if (!account)
|
|
148
|
+
throw new Error('Not authenticated');
|
|
149
|
+
const typedData = JSON.parse(typedDataJson);
|
|
150
|
+
return await account.signTypedData(typedData);
|
|
151
|
+
}
|
|
152
|
+
case 'wallet_switchEthereumChain': {
|
|
153
|
+
if (!params || params.length === 0) {
|
|
154
|
+
throw new Error('Missing chain parameter');
|
|
155
|
+
}
|
|
156
|
+
const [{ chainId }] = params;
|
|
157
|
+
const chainId_number = parseInt(chainId, 16);
|
|
158
|
+
// Update active chain
|
|
159
|
+
store.getState().setActiveChain(chainId_number);
|
|
160
|
+
// Emit chainChanged event
|
|
161
|
+
emitter.emit('chainChanged', chainId);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
default:
|
|
165
|
+
throw new Error(`Method not supported: ${method}`);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|