@volr/react 0.1.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/README.md +124 -0
- package/dist/index.cjs +2611 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +640 -0
- package/dist/index.d.ts +640 -0
- package/dist/index.js +2559 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2559 @@
|
|
|
1
|
+
import { createContext, useRef, useEffect, useMemo, useState, useCallback, useContext } from 'react';
|
|
2
|
+
import { signSession, getAuthNonce, signAuthorization, createPasskeyProvider, createMpcProvider, ZERO_HASH, deriveWrapKey, createMasterKeyProvider, sealMasterSeed, uploadBlob, deriveEvmKey, selectSigner, VolrError } from '@volr/sdk-core';
|
|
3
|
+
export { createMasterKeyProvider, createMpcProvider, createPasskeyProvider, deriveEvmKey, deriveWrapKey, sealMasterSeed, uploadBlob } from '@volr/sdk-core';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { jsx } from 'react/jsx-runtime';
|
|
6
|
+
import { encodeFunctionData, getAddress, createPublicClient, http } from 'viem';
|
|
7
|
+
|
|
8
|
+
// src/react/Provider.tsx
|
|
9
|
+
var VolrContext = createContext(null);
|
|
10
|
+
var InternalAuthContext = createContext(null);
|
|
11
|
+
function mapBackendError(error) {
|
|
12
|
+
return new VolrError(error.code, error.message);
|
|
13
|
+
}
|
|
14
|
+
function isErrorResponse(response) {
|
|
15
|
+
return !response.ok;
|
|
16
|
+
}
|
|
17
|
+
function unwrapResponse(response) {
|
|
18
|
+
if (isErrorResponse(response)) {
|
|
19
|
+
throw mapBackendError(response.error);
|
|
20
|
+
}
|
|
21
|
+
return response.data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/utils/ssr.ts
|
|
25
|
+
function isBrowser() {
|
|
26
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
27
|
+
}
|
|
28
|
+
var safeStorage = {
|
|
29
|
+
getItem(key) {
|
|
30
|
+
if (!isBrowser()) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const value = localStorage.getItem(key);
|
|
35
|
+
if (!value || value === "undefined" || value === "null") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
setItem(key, value) {
|
|
44
|
+
if (!isBrowser()) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
localStorage.setItem(key, value);
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
removeItem(key) {
|
|
53
|
+
if (!isBrowser()) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
localStorage.removeItem(key);
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
clear() {
|
|
62
|
+
if (!isBrowser()) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
localStorage.clear();
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// node_modules/@volr/shared/src/constants/storage.ts
|
|
73
|
+
var STORAGE_KEYS = {
|
|
74
|
+
accessToken: "volr:accessToken",
|
|
75
|
+
user: "volr:user",
|
|
76
|
+
provider: "volr:provider",
|
|
77
|
+
credentialId: "volr:credentialId",
|
|
78
|
+
selectedProject: "volr:selectedProject",
|
|
79
|
+
lastEmail: "volr:lastEmail"
|
|
80
|
+
};
|
|
81
|
+
var STORAGE_CHANNELS = {
|
|
82
|
+
session: "volr:session"
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/headless/client.ts
|
|
86
|
+
var APIClient = class {
|
|
87
|
+
constructor(config) {
|
|
88
|
+
this.refreshPromise = null;
|
|
89
|
+
this.accessToken = null;
|
|
90
|
+
this.apiKey = null;
|
|
91
|
+
this.apiKey = config.apiKey || null;
|
|
92
|
+
this.accessToken = safeStorage.getItem(STORAGE_KEYS.accessToken);
|
|
93
|
+
this.api = axios.create({
|
|
94
|
+
baseURL: config.baseUrl.replace(/\/+$/, ""),
|
|
95
|
+
withCredentials: true,
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "application/json"
|
|
98
|
+
},
|
|
99
|
+
transformRequest: [
|
|
100
|
+
(data) => {
|
|
101
|
+
if (data && typeof data === "object") {
|
|
102
|
+
return JSON.stringify(
|
|
103
|
+
data,
|
|
104
|
+
(_, value) => typeof value === "bigint" ? value.toString() : value
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
});
|
|
111
|
+
this.api.interceptors.request.use((config2) => {
|
|
112
|
+
if (!config2.headers) {
|
|
113
|
+
config2.headers = {};
|
|
114
|
+
}
|
|
115
|
+
if (this.apiKey) {
|
|
116
|
+
config2.headers["X-API-Key"] = this.apiKey;
|
|
117
|
+
} else {
|
|
118
|
+
console.warn("[APIClient] X-API-Key not set. Some endpoints may fail.");
|
|
119
|
+
}
|
|
120
|
+
if (this.accessToken) {
|
|
121
|
+
config2.headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
122
|
+
}
|
|
123
|
+
return config2;
|
|
124
|
+
});
|
|
125
|
+
this.api.interceptors.response.use(
|
|
126
|
+
(response) => response,
|
|
127
|
+
async (error) => {
|
|
128
|
+
const originalRequest = error.config;
|
|
129
|
+
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
130
|
+
originalRequest._retry = true;
|
|
131
|
+
try {
|
|
132
|
+
await this.refreshAccessToken();
|
|
133
|
+
if (this.accessToken) {
|
|
134
|
+
originalRequest.headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
135
|
+
}
|
|
136
|
+
return this.api(originalRequest);
|
|
137
|
+
} catch (refreshError) {
|
|
138
|
+
return Promise.reject(refreshError);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return Promise.reject(error);
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Set API key
|
|
147
|
+
*/
|
|
148
|
+
setApiKey(apiKey) {
|
|
149
|
+
this.apiKey = apiKey;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get API key
|
|
153
|
+
*/
|
|
154
|
+
getApiKey() {
|
|
155
|
+
return this.apiKey;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Set access token
|
|
159
|
+
*/
|
|
160
|
+
setAccessToken(token) {
|
|
161
|
+
this.accessToken = token;
|
|
162
|
+
if (token) {
|
|
163
|
+
safeStorage.setItem(STORAGE_KEYS.accessToken, token);
|
|
164
|
+
} else {
|
|
165
|
+
safeStorage.removeItem(STORAGE_KEYS.accessToken);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get access token
|
|
170
|
+
*/
|
|
171
|
+
getAccessToken() {
|
|
172
|
+
return this.accessToken;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Refresh access token (single-flight)
|
|
176
|
+
*/
|
|
177
|
+
async refreshAccessToken() {
|
|
178
|
+
if (this.refreshPromise) {
|
|
179
|
+
return this.refreshPromise;
|
|
180
|
+
}
|
|
181
|
+
this.refreshPromise = (async () => {
|
|
182
|
+
try {
|
|
183
|
+
const response = await this.api.post(
|
|
184
|
+
"/auth/refresh",
|
|
185
|
+
{}
|
|
186
|
+
);
|
|
187
|
+
const data = response.data;
|
|
188
|
+
if (isErrorResponse(data)) {
|
|
189
|
+
this.setAccessToken(null);
|
|
190
|
+
safeStorage.removeItem(STORAGE_KEYS.user);
|
|
191
|
+
throw new Error(data.error.message);
|
|
192
|
+
}
|
|
193
|
+
this.setAccessToken(data.data.accessToken);
|
|
194
|
+
if (data.data.user) {
|
|
195
|
+
safeStorage.setItem(STORAGE_KEYS.user, JSON.stringify(data.data.user));
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
this.refreshPromise = null;
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
return this.refreshPromise;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Make API request with automatic retry on 401
|
|
205
|
+
*/
|
|
206
|
+
async request(endpoint, options = {}, idempotencyKey) {
|
|
207
|
+
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
208
|
+
const config = {
|
|
209
|
+
method: options.method || "GET",
|
|
210
|
+
url: normalizedEndpoint
|
|
211
|
+
};
|
|
212
|
+
if (options.body) {
|
|
213
|
+
config.data = typeof options.body === "string" ? JSON.parse(options.body) : options.body;
|
|
214
|
+
}
|
|
215
|
+
if (idempotencyKey) {
|
|
216
|
+
config.headers = {
|
|
217
|
+
...config.headers,
|
|
218
|
+
"Idempotency-Key": idempotencyKey
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const response = await this.api.request(config);
|
|
222
|
+
return unwrapResponse(response.data);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* POST request
|
|
226
|
+
*/
|
|
227
|
+
async post(endpoint, body, idempotencyKey) {
|
|
228
|
+
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
229
|
+
const config = {
|
|
230
|
+
method: "POST",
|
|
231
|
+
url: normalizedEndpoint,
|
|
232
|
+
data: body
|
|
233
|
+
};
|
|
234
|
+
if (idempotencyKey) {
|
|
235
|
+
config.headers = {
|
|
236
|
+
"Idempotency-Key": idempotencyKey
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const response = await this.api.request(config);
|
|
240
|
+
return unwrapResponse(response.data);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* POST request that returns raw binary (ArrayBuffer)
|
|
244
|
+
* - Uses axios instance with interceptors (auto 401 refresh)
|
|
245
|
+
* - Applies X-API-Key and Authorization automatically
|
|
246
|
+
*/
|
|
247
|
+
async postBinary(endpoint, body) {
|
|
248
|
+
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
249
|
+
if (!this.apiKey) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
"API key not configured. Please ensure VolrProvider is initialized with projectApiKey in config."
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const response = await this.api.request({
|
|
255
|
+
method: "POST",
|
|
256
|
+
url: normalizedEndpoint,
|
|
257
|
+
data: body,
|
|
258
|
+
responseType: "arraybuffer",
|
|
259
|
+
headers: {
|
|
260
|
+
"X-API-Key": this.apiKey,
|
|
261
|
+
// Explicitly set API key header
|
|
262
|
+
...this.accessToken && { Authorization: `Bearer ${this.accessToken}` }
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return response.data;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* GET request
|
|
269
|
+
*/
|
|
270
|
+
async get(endpoint) {
|
|
271
|
+
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
272
|
+
const response = await this.api.get(normalizedEndpoint);
|
|
273
|
+
return unwrapResponse(response.data);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/headless/auth-sync.ts
|
|
278
|
+
var SessionSync = class {
|
|
279
|
+
constructor() {
|
|
280
|
+
this.channel = null;
|
|
281
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
282
|
+
if (typeof window !== "undefined" && "BroadcastChannel" in window) {
|
|
283
|
+
this.channel = new BroadcastChannel(STORAGE_CHANNELS.session);
|
|
284
|
+
this.channel.onmessage = (e) => {
|
|
285
|
+
this.handleChannelMessage(e.data);
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (typeof window !== "undefined") {
|
|
289
|
+
window.addEventListener("storage", (e) => {
|
|
290
|
+
if (e.key === STORAGE_KEYS.accessToken || e.key === STORAGE_KEYS.user) {
|
|
291
|
+
this.notifyListeners({
|
|
292
|
+
type: "REFRESH",
|
|
293
|
+
payload: {
|
|
294
|
+
accessToken: safeStorage.getItem(STORAGE_KEYS.accessToken) || ""
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Subscribe to session events
|
|
303
|
+
*/
|
|
304
|
+
subscribe(listener) {
|
|
305
|
+
this.listeners.add(listener);
|
|
306
|
+
return () => {
|
|
307
|
+
this.listeners.delete(listener);
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Broadcast session event to other tabs
|
|
312
|
+
*/
|
|
313
|
+
broadcast(event) {
|
|
314
|
+
if (this.channel) {
|
|
315
|
+
this.channel.postMessage(event);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Handle channel message
|
|
320
|
+
*/
|
|
321
|
+
handleChannelMessage(event) {
|
|
322
|
+
this.notifyListeners(event);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Notify all listeners
|
|
326
|
+
*/
|
|
327
|
+
notifyListeners(event) {
|
|
328
|
+
for (const listener of this.listeners) {
|
|
329
|
+
try {
|
|
330
|
+
listener(event);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error("SessionSync listener error:", error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Cleanup
|
|
338
|
+
*/
|
|
339
|
+
destroy() {
|
|
340
|
+
if (this.channel) {
|
|
341
|
+
this.channel.close();
|
|
342
|
+
this.channel = null;
|
|
343
|
+
}
|
|
344
|
+
this.listeners.clear();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// src/config/webauthn.ts
|
|
349
|
+
var AUTHENTICATOR_SELECTION = {
|
|
350
|
+
authenticatorAttachment: "platform",
|
|
351
|
+
userVerification: "required",
|
|
352
|
+
residentKey: "required"
|
|
353
|
+
};
|
|
354
|
+
var CREDENTIAL_MEDIATION = "required";
|
|
355
|
+
var USER_VERIFICATION = "required";
|
|
356
|
+
var WEBAUTHN_TIMEOUT = 6e4;
|
|
357
|
+
var PUBKEY_CRED_PARAMS = [
|
|
358
|
+
{
|
|
359
|
+
type: "public-key",
|
|
360
|
+
alg: -7
|
|
361
|
+
// ES256 (P-256)
|
|
362
|
+
}
|
|
363
|
+
];
|
|
364
|
+
var ATTESTATION = "direct";
|
|
365
|
+
|
|
366
|
+
// src/adapters/passkey.ts
|
|
367
|
+
function createPasskeyAdapter(options = {}) {
|
|
368
|
+
const rpId = options.rpId || (typeof window !== "undefined" ? window.location.hostname : "localhost");
|
|
369
|
+
return {
|
|
370
|
+
async getPublicKey() {
|
|
371
|
+
const credential = await navigator.credentials.get({
|
|
372
|
+
publicKey: {
|
|
373
|
+
challenge: new Uint8Array(32),
|
|
374
|
+
// Dummy challenge for getting public key
|
|
375
|
+
allowCredentials: [],
|
|
376
|
+
// Empty = any credential
|
|
377
|
+
userVerification: "required"
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
if (!credential || !("response" in credential)) {
|
|
381
|
+
throw new Error("Failed to get passkey credential");
|
|
382
|
+
}
|
|
383
|
+
throw new Error(
|
|
384
|
+
"getPublicKey not implemented - use credential creation response"
|
|
385
|
+
);
|
|
386
|
+
},
|
|
387
|
+
async signP256(msgHash32) {
|
|
388
|
+
if (msgHash32.length !== 32) {
|
|
389
|
+
throw new Error("Message hash must be 32 bytes");
|
|
390
|
+
}
|
|
391
|
+
const credential = await navigator.credentials.get({
|
|
392
|
+
publicKey: {
|
|
393
|
+
challenge: msgHash32,
|
|
394
|
+
allowCredentials: [],
|
|
395
|
+
// Empty = any credential
|
|
396
|
+
userVerification: "required"
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
if (!credential || !("response" in credential)) {
|
|
400
|
+
throw new Error("Failed to get passkey credential for signing");
|
|
401
|
+
}
|
|
402
|
+
const response = credential.response;
|
|
403
|
+
const signature = response.signature;
|
|
404
|
+
const derSignature = new Uint8Array(signature);
|
|
405
|
+
if (derSignature[0] !== 48) {
|
|
406
|
+
throw new Error("Invalid DER signature format");
|
|
407
|
+
}
|
|
408
|
+
let offset = 2;
|
|
409
|
+
if (derSignature[offset] !== 2) {
|
|
410
|
+
throw new Error("Invalid DER signature: expected r marker");
|
|
411
|
+
}
|
|
412
|
+
offset++;
|
|
413
|
+
const rLength = derSignature[offset++];
|
|
414
|
+
const rBytes = derSignature.slice(offset, offset + rLength);
|
|
415
|
+
offset += rLength;
|
|
416
|
+
if (derSignature[offset] !== 2) {
|
|
417
|
+
throw new Error("Invalid DER signature: expected s marker");
|
|
418
|
+
}
|
|
419
|
+
offset++;
|
|
420
|
+
const sLength = derSignature[offset++];
|
|
421
|
+
const sBytes = derSignature.slice(offset, offset + sLength);
|
|
422
|
+
const r = new Uint8Array(32);
|
|
423
|
+
const s = new Uint8Array(32);
|
|
424
|
+
if (rBytes.length > 32 || sBytes.length > 32) {
|
|
425
|
+
throw new Error("Signature r or s exceeds 32 bytes");
|
|
426
|
+
}
|
|
427
|
+
r.set(rBytes, 32 - rBytes.length);
|
|
428
|
+
s.set(sBytes, 32 - sBytes.length);
|
|
429
|
+
return { r, s };
|
|
430
|
+
},
|
|
431
|
+
async authenticate(prfInput) {
|
|
432
|
+
if (!prfInput.credentialId) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
"[PasskeyAdapter] credentialId is required for authentication. This usually means the passkey was not properly registered or user data is incomplete. Please re-enroll your passkey."
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const hexString = prfInput.credentialId.startsWith("0x") ? prfInput.credentialId.slice(2) : prfInput.credentialId;
|
|
438
|
+
if (!/^[0-9a-fA-F]+$/.test(hexString)) {
|
|
439
|
+
console.error(
|
|
440
|
+
"[PasskeyAdapter] Invalid credentialId format (not hex):",
|
|
441
|
+
prfInput.credentialId
|
|
442
|
+
);
|
|
443
|
+
throw new Error(
|
|
444
|
+
`[PasskeyAdapter] Invalid credentialId format. Expected hex string, got: ${prfInput.credentialId}`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const normalizedHex = hexString.length % 2 === 0 ? hexString : "0" + hexString;
|
|
448
|
+
const hexPairs = normalizedHex.match(/.{1,2}/g);
|
|
449
|
+
if (!hexPairs) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`[PasskeyAdapter] Failed to parse hex string: ${normalizedHex}`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const credIdBytes = new Uint8Array(
|
|
455
|
+
hexPairs.map((byte) => parseInt(byte, 16))
|
|
456
|
+
);
|
|
457
|
+
const reconvertedHex = Array.from(credIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
458
|
+
if (reconvertedHex.toLowerCase() !== normalizedHex.toLowerCase()) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`[PasskeyAdapter] credentialId conversion failed. Original: ${normalizedHex}, Reconverted: ${reconvertedHex}`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const allowCredentials = [
|
|
464
|
+
{
|
|
465
|
+
id: credIdBytes,
|
|
466
|
+
type: "public-key"
|
|
467
|
+
}
|
|
468
|
+
];
|
|
469
|
+
let credential = null;
|
|
470
|
+
try {
|
|
471
|
+
credential = await navigator.credentials.get({
|
|
472
|
+
publicKey: {
|
|
473
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
474
|
+
rpId,
|
|
475
|
+
allowCredentials,
|
|
476
|
+
userVerification: USER_VERIFICATION,
|
|
477
|
+
// Shared constant
|
|
478
|
+
extensions: {
|
|
479
|
+
prf: {
|
|
480
|
+
eval: {
|
|
481
|
+
first: prfInput.salt.buffer
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
mediation: CREDENTIAL_MEDIATION
|
|
487
|
+
// Use shared constant
|
|
488
|
+
});
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.error("[PasskeyAdapter] WebAuthn get() failed:", error);
|
|
491
|
+
console.error("[PasskeyAdapter] Error name:", error?.name);
|
|
492
|
+
console.error("[PasskeyAdapter] Error message:", error?.message);
|
|
493
|
+
if (error?.name === "NotAllowedError") {
|
|
494
|
+
throw new Error(
|
|
495
|
+
"[PasskeyAdapter] User cancelled the passkey prompt or authentication was denied. Please try again and complete the authentication."
|
|
496
|
+
);
|
|
497
|
+
} else if (error?.name === "NotFoundError" || error?.name === "InvalidStateError") {
|
|
498
|
+
throw new Error(
|
|
499
|
+
"[PasskeyAdapter] No passkey found matching the provided credentialId. This may happen if the passkey was deleted or the credentialId is incorrect. Please re-enroll your passkey."
|
|
500
|
+
);
|
|
501
|
+
} else if (error?.name === "NotSupportedError") {
|
|
502
|
+
throw new Error(
|
|
503
|
+
"[PasskeyAdapter] WebAuthn PRF extension is not supported by this browser or device. Please use a browser that supports WebAuthn PRF extension (Chrome 108+, Edge 108+, Safari 16.4+)."
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
throw new Error(
|
|
507
|
+
`[PasskeyAdapter] WebAuthn authentication failed: ${error?.message || "Unknown error"}`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (!credential || !credential.response) {
|
|
511
|
+
console.error(
|
|
512
|
+
"[PasskeyAdapter] credential is null or missing response"
|
|
513
|
+
);
|
|
514
|
+
throw new Error(
|
|
515
|
+
"[PasskeyAdapter] Failed to get passkey credential for PRF. The passkey prompt may have been cancelled or no matching credential was found."
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
const extensionResults = credential.getClientExtensionResults();
|
|
519
|
+
if (!extensionResults.prf || !extensionResults.prf.results || !extensionResults.prf.results.first) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
"[PasskeyAdapter] PRF extension not supported or PRF output missing"
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
const prfOutputBuffer = extensionResults.prf.results.first;
|
|
525
|
+
const prfOutput = new Uint8Array(prfOutputBuffer);
|
|
526
|
+
const credentialIdBytes = new Uint8Array(credential.rawId);
|
|
527
|
+
const credentialIdBase64 = btoa(String.fromCharCode(...credentialIdBytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
528
|
+
console.log("[PasskeyAdapter] WebAuthn prompt completed successfully");
|
|
529
|
+
return {
|
|
530
|
+
prfOutput,
|
|
531
|
+
credentialId: credentialIdBase64
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/headless/passkey-restore.ts
|
|
538
|
+
async function restorePasskey(params) {
|
|
539
|
+
const {
|
|
540
|
+
client,
|
|
541
|
+
userId,
|
|
542
|
+
blobUrl,
|
|
543
|
+
prfInput,
|
|
544
|
+
credentialId: providedCredentialId
|
|
545
|
+
} = params;
|
|
546
|
+
const credentialId = providedCredentialId || prfInput.credentialId || safeStorage.getItem(STORAGE_KEYS.credentialId);
|
|
547
|
+
if (!credentialId) {
|
|
548
|
+
throw new Error(
|
|
549
|
+
"Credential ID not found. Please provide credentialId in params, prfInput, or ensure it is stored in localStorage. If you recently enrolled a passkey, try refreshing the page or re-enrolling."
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
console.log("[restorePasskey] Using credentialId:", credentialId ? "present" : "MISSING");
|
|
553
|
+
console.log(
|
|
554
|
+
"[restorePasskey] credentialId source:",
|
|
555
|
+
providedCredentialId ? "params" : prfInput.credentialId ? "prfInput" : "localStorage"
|
|
556
|
+
);
|
|
557
|
+
console.log("[restorePasskey] prfInput:", prfInput);
|
|
558
|
+
console.log("[restorePasskey] Step 1: Ensuring access token is fresh...");
|
|
559
|
+
const currentToken = client.getAccessToken();
|
|
560
|
+
if (!currentToken) {
|
|
561
|
+
console.log("[restorePasskey] No access token found, calling /auth/refresh...");
|
|
562
|
+
try {
|
|
563
|
+
await client.post("/auth/refresh", {});
|
|
564
|
+
console.log("[restorePasskey] Access token refreshed successfully");
|
|
565
|
+
} catch (error) {
|
|
566
|
+
console.error("[restorePasskey] Failed to refresh access token:", error);
|
|
567
|
+
throw new Error("Failed to refresh access token. Please log in again.");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
console.log("[restorePasskey] Step 2: Downloading blob via backend proxy for blobUrl:", blobUrl);
|
|
571
|
+
const apiKey = client.getApiKey();
|
|
572
|
+
if (!apiKey) {
|
|
573
|
+
console.error("[restorePasskey] API key not set in client. This is required for blob download.");
|
|
574
|
+
throw new Error(
|
|
575
|
+
"API key not configured. Please ensure VolrProvider is initialized with projectApiKey in config."
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
console.log("[restorePasskey] API key is set:", apiKey ? "present" : "MISSING");
|
|
579
|
+
const arrayBuffer = await client.postBinary("/blob/download", { key: blobUrl });
|
|
580
|
+
const blobBytes = new Uint8Array(arrayBuffer);
|
|
581
|
+
const nonceLength = 12;
|
|
582
|
+
const cipherLength = blobBytes.length - nonceLength;
|
|
583
|
+
if (cipherLength <= 0) {
|
|
584
|
+
throw new Error("Invalid blob format: blob too small");
|
|
585
|
+
}
|
|
586
|
+
const cipher = blobBytes.slice(0, cipherLength);
|
|
587
|
+
const nonce = blobBytes.slice(cipherLength);
|
|
588
|
+
const keyStorageType = "passkey";
|
|
589
|
+
const version = "v1";
|
|
590
|
+
const aadBytes = new TextEncoder().encode(
|
|
591
|
+
`volr/master-seed/v1|${userId}|${keyStorageType}|${version}`
|
|
592
|
+
);
|
|
593
|
+
const passkeyAdapter = createPasskeyAdapter({
|
|
594
|
+
rpId: typeof window !== "undefined" ? window.location.hostname : "localhost"});
|
|
595
|
+
const provider = createPasskeyProvider(passkeyAdapter, {
|
|
596
|
+
prfInput: {
|
|
597
|
+
...prfInput,
|
|
598
|
+
credentialId
|
|
599
|
+
// Add credentialId to prfInput
|
|
600
|
+
},
|
|
601
|
+
encryptedBlob: {
|
|
602
|
+
cipher,
|
|
603
|
+
nonce
|
|
604
|
+
},
|
|
605
|
+
aad: aadBytes
|
|
606
|
+
});
|
|
607
|
+
console.log("[restorePasskey] Provider created with credentialId:", credentialId);
|
|
608
|
+
return {
|
|
609
|
+
provider
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function serializeBigIntDeep(obj) {
|
|
613
|
+
if (obj === null || obj === void 0) {
|
|
614
|
+
return obj;
|
|
615
|
+
}
|
|
616
|
+
if (typeof obj === "bigint") {
|
|
617
|
+
return obj.toString();
|
|
618
|
+
}
|
|
619
|
+
if (Array.isArray(obj)) {
|
|
620
|
+
return obj.map(serializeBigIntDeep);
|
|
621
|
+
}
|
|
622
|
+
if (typeof obj === "object") {
|
|
623
|
+
const result = {};
|
|
624
|
+
for (const key in obj) {
|
|
625
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
626
|
+
result[key] = serializeBigIntDeep(obj[key]);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
631
|
+
return obj;
|
|
632
|
+
}
|
|
633
|
+
function VolrProvider({ config, children }) {
|
|
634
|
+
const REQUIRE_USER_GESTURE_TO_RESTORE = true;
|
|
635
|
+
const providerCountRef = useRef(0);
|
|
636
|
+
useEffect(() => {
|
|
637
|
+
providerCountRef.current++;
|
|
638
|
+
if (providerCountRef.current > 1) {
|
|
639
|
+
console.warn(
|
|
640
|
+
"Multiple VolrProvider components detected. This may cause session synchronization issues."
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
return () => {
|
|
644
|
+
providerCountRef.current--;
|
|
645
|
+
};
|
|
646
|
+
}, []);
|
|
647
|
+
const client = useMemo(() => {
|
|
648
|
+
if (!config.projectApiKey) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
"VolrProvider requires config.projectApiKey. Please set VITE_PROJECT_API_KEY environment variable or provide projectApiKey in config."
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
const baseUrl = config.__devApiBaseUrl || config.apiBaseUrl;
|
|
654
|
+
return new APIClient({
|
|
655
|
+
baseUrl,
|
|
656
|
+
apiKey: config.projectApiKey
|
|
657
|
+
});
|
|
658
|
+
}, [config.apiBaseUrl, config.projectApiKey]);
|
|
659
|
+
const [user, setUser] = useState(() => {
|
|
660
|
+
if (typeof window === "undefined") return null;
|
|
661
|
+
const userStr = safeStorage.getItem(STORAGE_KEYS.user);
|
|
662
|
+
if (!userStr || userStr === "undefined" || userStr === "null") {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
return JSON.parse(userStr);
|
|
667
|
+
} catch {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
const [provider, setProviderState] = useState(
|
|
672
|
+
null
|
|
673
|
+
);
|
|
674
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
675
|
+
const [error, setError] = useState(null);
|
|
676
|
+
const [chainId] = useState(config.defaultChainId);
|
|
677
|
+
const [accessToken, setAccessTokenState] = useState(() => {
|
|
678
|
+
return client.getAccessToken();
|
|
679
|
+
});
|
|
680
|
+
const syncRef = useRef(null);
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
syncRef.current = new SessionSync();
|
|
683
|
+
const unsubscribe = syncRef.current.subscribe((event) => {
|
|
684
|
+
if (event.type === "LOGIN") {
|
|
685
|
+
client.setAccessToken(event.payload.accessToken);
|
|
686
|
+
setAccessTokenState(event.payload.accessToken);
|
|
687
|
+
setUser(event.payload.user);
|
|
688
|
+
safeStorage.setItem(
|
|
689
|
+
STORAGE_KEYS.user,
|
|
690
|
+
JSON.stringify(event.payload.user)
|
|
691
|
+
);
|
|
692
|
+
client.setApiKey(config.projectApiKey);
|
|
693
|
+
} else if (event.type === "LOGOUT") {
|
|
694
|
+
client.setAccessToken(null);
|
|
695
|
+
setAccessTokenState(null);
|
|
696
|
+
setUser(null);
|
|
697
|
+
setProviderState(null);
|
|
698
|
+
safeStorage.removeItem(STORAGE_KEYS.user);
|
|
699
|
+
safeStorage.removeItem(STORAGE_KEYS.provider);
|
|
700
|
+
} else if (event.type === "REFRESH") {
|
|
701
|
+
client.setAccessToken(event.payload.accessToken);
|
|
702
|
+
setAccessTokenState(event.payload.accessToken);
|
|
703
|
+
} else if (event.type === "PROVIDER_SET") {
|
|
704
|
+
setUser((prev) => ({
|
|
705
|
+
...prev,
|
|
706
|
+
keyStorageType: event.payload.keyStorageType,
|
|
707
|
+
address: event.payload.address
|
|
708
|
+
}));
|
|
709
|
+
safeStorage.setItem(STORAGE_KEYS.provider, event.payload.keyStorageType);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
return () => {
|
|
713
|
+
unsubscribe();
|
|
714
|
+
syncRef.current?.destroy();
|
|
715
|
+
};
|
|
716
|
+
}, [client, config.projectApiKey]);
|
|
717
|
+
const setProvider = useCallback(
|
|
718
|
+
async (newProvider) => {
|
|
719
|
+
try {
|
|
720
|
+
setError(null);
|
|
721
|
+
const keyStorageType = newProvider.keyStorageType;
|
|
722
|
+
setProviderState(newProvider);
|
|
723
|
+
try {
|
|
724
|
+
const refreshResponse = await client.post(
|
|
725
|
+
"/auth/refresh",
|
|
726
|
+
{}
|
|
727
|
+
);
|
|
728
|
+
if (refreshResponse.user) {
|
|
729
|
+
console.log(
|
|
730
|
+
"[Provider] setProvider: User data refreshed from backend:",
|
|
731
|
+
refreshResponse.user
|
|
732
|
+
);
|
|
733
|
+
setUser(refreshResponse.user);
|
|
734
|
+
safeStorage.setItem(
|
|
735
|
+
STORAGE_KEYS.user,
|
|
736
|
+
JSON.stringify(refreshResponse.user)
|
|
737
|
+
);
|
|
738
|
+
} else {
|
|
739
|
+
setUser((prev) => ({
|
|
740
|
+
...prev,
|
|
741
|
+
keyStorageType
|
|
742
|
+
}));
|
|
743
|
+
safeStorage.setItem(
|
|
744
|
+
STORAGE_KEYS.user,
|
|
745
|
+
JSON.stringify({
|
|
746
|
+
...user,
|
|
747
|
+
keyStorageType
|
|
748
|
+
})
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
} catch (error2) {
|
|
752
|
+
console.warn(
|
|
753
|
+
"[Provider] setProvider: Failed to refresh user data, using partial update:",
|
|
754
|
+
error2
|
|
755
|
+
);
|
|
756
|
+
setUser((prev) => ({
|
|
757
|
+
...prev,
|
|
758
|
+
keyStorageType
|
|
759
|
+
}));
|
|
760
|
+
safeStorage.setItem(
|
|
761
|
+
STORAGE_KEYS.user,
|
|
762
|
+
JSON.stringify({
|
|
763
|
+
...user,
|
|
764
|
+
keyStorageType
|
|
765
|
+
})
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
safeStorage.setItem(STORAGE_KEYS.provider, keyStorageType);
|
|
769
|
+
syncRef.current?.broadcast({
|
|
770
|
+
type: "PROVIDER_SET",
|
|
771
|
+
payload: {
|
|
772
|
+
keyStorageType
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
} catch (err) {
|
|
776
|
+
const error2 = err instanceof Error ? err : new Error("Failed to set provider");
|
|
777
|
+
setError(error2);
|
|
778
|
+
throw error2;
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
[client, user]
|
|
782
|
+
);
|
|
783
|
+
const hasRecoveredRef = useRef(false);
|
|
784
|
+
const restorationAttemptedRef = useRef(false);
|
|
785
|
+
useEffect(() => {
|
|
786
|
+
if (config.autoRecoverOnLogin !== false && !hasRecoveredRef.current) {
|
|
787
|
+
hasRecoveredRef.current = true;
|
|
788
|
+
const recover = async () => {
|
|
789
|
+
try {
|
|
790
|
+
setIsLoading(true);
|
|
791
|
+
const response = await client.post(
|
|
792
|
+
"/auth/refresh",
|
|
793
|
+
{}
|
|
794
|
+
);
|
|
795
|
+
console.log("[Provider] /auth/refresh response:", response);
|
|
796
|
+
const refreshedToken = client.getAccessToken();
|
|
797
|
+
if (refreshedToken) {
|
|
798
|
+
if (response.user) {
|
|
799
|
+
console.log(
|
|
800
|
+
"[Provider] Setting user from response:",
|
|
801
|
+
response.user
|
|
802
|
+
);
|
|
803
|
+
console.log("[Provider] User fields:", {
|
|
804
|
+
id: response.user.id,
|
|
805
|
+
email: response.user.email,
|
|
806
|
+
accountId: response.user.accountId,
|
|
807
|
+
evmAddress: response.user.evmAddress,
|
|
808
|
+
signerType: response.user.signerType,
|
|
809
|
+
keyStorageType: response.user.keyStorageType,
|
|
810
|
+
walletConnector: response.user.walletConnector,
|
|
811
|
+
blobUrl: response.user.blobUrl,
|
|
812
|
+
prfInput: response.user.prfInput,
|
|
813
|
+
credentialId: response.user.credentialId
|
|
814
|
+
});
|
|
815
|
+
setUser(response.user);
|
|
816
|
+
safeStorage.setItem(
|
|
817
|
+
STORAGE_KEYS.user,
|
|
818
|
+
JSON.stringify(response.user)
|
|
819
|
+
);
|
|
820
|
+
if (!REQUIRE_USER_GESTURE_TO_RESTORE) ; else {
|
|
821
|
+
if (response.user.keyStorageType === "passkey") {
|
|
822
|
+
console.log(
|
|
823
|
+
"[Provider] TTL=0 mode: Provider restoration deferred until transaction (requires user gesture)"
|
|
824
|
+
);
|
|
825
|
+
if (!response.user.blobUrl || !response.user.prfInput) {
|
|
826
|
+
console.warn(
|
|
827
|
+
"[Provider] Passkey user detected but missing blobUrl or prfInput. User needs to re-enroll passkey."
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
} else {
|
|
833
|
+
console.log(
|
|
834
|
+
"[Provider] No user in response, loading from storage"
|
|
835
|
+
);
|
|
836
|
+
const userStr = safeStorage.getItem(STORAGE_KEYS.user);
|
|
837
|
+
if (userStr) {
|
|
838
|
+
try {
|
|
839
|
+
const storedUser = JSON.parse(userStr);
|
|
840
|
+
console.log(
|
|
841
|
+
"[Provider] Loaded user from storage:",
|
|
842
|
+
storedUser
|
|
843
|
+
);
|
|
844
|
+
setUser(storedUser);
|
|
845
|
+
} catch {
|
|
846
|
+
safeStorage.removeItem(STORAGE_KEYS.user);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
setAccessTokenState(refreshedToken);
|
|
851
|
+
}
|
|
852
|
+
} catch {
|
|
853
|
+
client.setAccessToken(null);
|
|
854
|
+
setAccessTokenState(null);
|
|
855
|
+
setUser(null);
|
|
856
|
+
setProviderState(null);
|
|
857
|
+
safeStorage.removeItem(STORAGE_KEYS.user);
|
|
858
|
+
safeStorage.removeItem(STORAGE_KEYS.provider);
|
|
859
|
+
} finally {
|
|
860
|
+
setIsLoading(false);
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
recover();
|
|
864
|
+
} else {
|
|
865
|
+
setIsLoading(false);
|
|
866
|
+
}
|
|
867
|
+
}, [client, config.autoRecoverOnLogin]);
|
|
868
|
+
const precheck = useCallback(
|
|
869
|
+
async (input) => {
|
|
870
|
+
try {
|
|
871
|
+
setError(null);
|
|
872
|
+
if (!client.getAccessToken()) {
|
|
873
|
+
try {
|
|
874
|
+
await client.post("/auth/refresh", {});
|
|
875
|
+
} catch {
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const response = await client.post(
|
|
879
|
+
"/wallet/precheck",
|
|
880
|
+
input
|
|
881
|
+
);
|
|
882
|
+
return response.quote;
|
|
883
|
+
} catch (err) {
|
|
884
|
+
const error2 = err instanceof Error ? err : new Error("Precheck failed");
|
|
885
|
+
const diag = err?.response?.data?.error?.developerDiagnostics;
|
|
886
|
+
if (diag) {
|
|
887
|
+
console.error("[volr][precheck] developerDiagnostics:", diag);
|
|
888
|
+
}
|
|
889
|
+
setError(error2);
|
|
890
|
+
throw error2;
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
[client]
|
|
894
|
+
);
|
|
895
|
+
const relay = useCallback(
|
|
896
|
+
async (input, opts) => {
|
|
897
|
+
try {
|
|
898
|
+
setError(null);
|
|
899
|
+
if (!client.getAccessToken()) {
|
|
900
|
+
try {
|
|
901
|
+
await client.post("/auth/refresh", {});
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const idempotencyKey = opts?.idempotencyKey ?? (typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`);
|
|
906
|
+
const serializedInput = serializeBigIntDeep(input);
|
|
907
|
+
const response = await client.post(
|
|
908
|
+
"/wallet/relay",
|
|
909
|
+
serializedInput,
|
|
910
|
+
idempotencyKey
|
|
911
|
+
);
|
|
912
|
+
return response;
|
|
913
|
+
} catch (err) {
|
|
914
|
+
const error2 = err instanceof Error ? err : new Error("Relay failed");
|
|
915
|
+
const diag = err?.response?.data?.error?.developerDiagnostics;
|
|
916
|
+
if (diag) {
|
|
917
|
+
console.error("[volr][relay] developerDiagnostics:", diag);
|
|
918
|
+
}
|
|
919
|
+
setError(error2);
|
|
920
|
+
throw error2;
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
[client]
|
|
924
|
+
);
|
|
925
|
+
const logout = useCallback(async () => {
|
|
926
|
+
try {
|
|
927
|
+
setError(null);
|
|
928
|
+
client.setAccessToken(null);
|
|
929
|
+
setUser(null);
|
|
930
|
+
setProviderState(null);
|
|
931
|
+
safeStorage.removeItem(STORAGE_KEYS.user);
|
|
932
|
+
safeStorage.removeItem(STORAGE_KEYS.accessToken);
|
|
933
|
+
safeStorage.removeItem(STORAGE_KEYS.provider);
|
|
934
|
+
syncRef.current?.broadcast({ type: "LOGOUT" });
|
|
935
|
+
} catch (err) {
|
|
936
|
+
const error2 = err instanceof Error ? err : new Error("Logout failed");
|
|
937
|
+
setError(error2);
|
|
938
|
+
throw error2;
|
|
939
|
+
}
|
|
940
|
+
}, [client]);
|
|
941
|
+
useEffect(() => {
|
|
942
|
+
if (!user) return;
|
|
943
|
+
if (typeof window === "undefined" || !window.ethereum) {
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const ethereum = window.ethereum;
|
|
947
|
+
const handleAccountsChanged = (accounts) => {
|
|
948
|
+
console.log("[Provider] accountsChanged event:", accounts);
|
|
949
|
+
if (accounts.length === 0) {
|
|
950
|
+
console.warn("[Provider] Wallet disconnected, logging out...");
|
|
951
|
+
setTimeout(() => logout(), 3e3);
|
|
952
|
+
} else if (user.evmAddress && accounts[0].toLowerCase() !== user.evmAddress.toLowerCase()) {
|
|
953
|
+
console.warn("[Provider] Account changed, logging out in 3 seconds...");
|
|
954
|
+
setTimeout(() => logout(), 3e3);
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
const handleChainChanged = (chainIdHex) => {
|
|
958
|
+
const newChainId = parseInt(chainIdHex, 16);
|
|
959
|
+
console.log("[Provider] chainChanged event:", newChainId);
|
|
960
|
+
setUser(
|
|
961
|
+
(prev) => prev ? { ...prev, lastWalletChainId: newChainId } : null
|
|
962
|
+
);
|
|
963
|
+
window.location.reload();
|
|
964
|
+
};
|
|
965
|
+
const handleDisconnect = () => {
|
|
966
|
+
console.log("[Provider] disconnect event");
|
|
967
|
+
setProviderState(null);
|
|
968
|
+
safeStorage.removeItem(STORAGE_KEYS.provider);
|
|
969
|
+
};
|
|
970
|
+
ethereum.on("accountsChanged", handleAccountsChanged);
|
|
971
|
+
ethereum.on("chainChanged", handleChainChanged);
|
|
972
|
+
ethereum.on("disconnect", handleDisconnect);
|
|
973
|
+
return () => {
|
|
974
|
+
ethereum.removeListener("accountsChanged", handleAccountsChanged);
|
|
975
|
+
ethereum.removeListener("chainChanged", handleChainChanged);
|
|
976
|
+
ethereum.removeListener("disconnect", handleDisconnect);
|
|
977
|
+
};
|
|
978
|
+
}, [user, logout, setUser]);
|
|
979
|
+
const publicValue = useMemo(
|
|
980
|
+
() => ({
|
|
981
|
+
config,
|
|
982
|
+
user,
|
|
983
|
+
provider,
|
|
984
|
+
setProvider,
|
|
985
|
+
setUser: (newUser) => {
|
|
986
|
+
setUser(newUser);
|
|
987
|
+
if (newUser) {
|
|
988
|
+
safeStorage.setItem(STORAGE_KEYS.user, JSON.stringify(newUser));
|
|
989
|
+
} else {
|
|
990
|
+
safeStorage.removeItem(STORAGE_KEYS.user);
|
|
991
|
+
}
|
|
992
|
+
},
|
|
993
|
+
chainId,
|
|
994
|
+
precheck,
|
|
995
|
+
relay,
|
|
996
|
+
logout,
|
|
997
|
+
isLoading,
|
|
998
|
+
error
|
|
999
|
+
}),
|
|
1000
|
+
[
|
|
1001
|
+
config,
|
|
1002
|
+
user,
|
|
1003
|
+
provider,
|
|
1004
|
+
setProvider,
|
|
1005
|
+
chainId,
|
|
1006
|
+
precheck,
|
|
1007
|
+
relay,
|
|
1008
|
+
logout,
|
|
1009
|
+
isLoading,
|
|
1010
|
+
error
|
|
1011
|
+
]
|
|
1012
|
+
);
|
|
1013
|
+
const internalValue = useMemo(
|
|
1014
|
+
() => ({
|
|
1015
|
+
session: {
|
|
1016
|
+
accessToken,
|
|
1017
|
+
refreshToken: null
|
|
1018
|
+
// Cookie-based, opaque
|
|
1019
|
+
},
|
|
1020
|
+
refreshAccessToken: async () => {
|
|
1021
|
+
await client.post("/auth/refresh", {});
|
|
1022
|
+
const token = client.getAccessToken();
|
|
1023
|
+
setAccessTokenState(token);
|
|
1024
|
+
},
|
|
1025
|
+
setAccessToken: (token) => {
|
|
1026
|
+
client.setAccessToken(token);
|
|
1027
|
+
setAccessTokenState(token);
|
|
1028
|
+
},
|
|
1029
|
+
client
|
|
1030
|
+
// Expose APIClient instance
|
|
1031
|
+
}),
|
|
1032
|
+
[client, accessToken]
|
|
1033
|
+
);
|
|
1034
|
+
return /* @__PURE__ */ jsx(VolrContext.Provider, { value: publicValue, children: /* @__PURE__ */ jsx(InternalAuthContext.Provider, { value: internalValue, children }) });
|
|
1035
|
+
}
|
|
1036
|
+
function useVolr() {
|
|
1037
|
+
const context = useContext(VolrContext);
|
|
1038
|
+
if (!context) {
|
|
1039
|
+
throw new Error("useVolr must be used within VolrProvider");
|
|
1040
|
+
}
|
|
1041
|
+
return context;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/utils/validation.ts
|
|
1045
|
+
var DEFAULT_LIMITS = {
|
|
1046
|
+
MAX_CALLS: 8,
|
|
1047
|
+
MAX_GAS_LIMIT: 10000000n,
|
|
1048
|
+
MAX_DATA_BYTES: 1e5
|
|
1049
|
+
};
|
|
1050
|
+
function validatePolicyId(policyId) {
|
|
1051
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(policyId)) {
|
|
1052
|
+
throw new Error("policyId must be 0x-prefixed 32-byte hex string");
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function validateCallValue(value) {
|
|
1056
|
+
if (value !== 0n) {
|
|
1057
|
+
throw new Error("call.value must be 0 (policy does not allow ETH transfers)");
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function validateCalls(calls, config = {}) {
|
|
1061
|
+
const maxCalls = config.maxCalls ?? DEFAULT_LIMITS.MAX_CALLS;
|
|
1062
|
+
const maxGasLimit = config.maxGasLimit ?? DEFAULT_LIMITS.MAX_GAS_LIMIT;
|
|
1063
|
+
const maxDataBytes = config.maxDataBytes ?? DEFAULT_LIMITS.MAX_DATA_BYTES;
|
|
1064
|
+
if (!calls || calls.length === 0) {
|
|
1065
|
+
throw new Error("calls must not be empty");
|
|
1066
|
+
}
|
|
1067
|
+
if (calls.length > maxCalls) {
|
|
1068
|
+
throw new Error(`calls.length (${calls.length}) exceeds maximum (${maxCalls})`);
|
|
1069
|
+
}
|
|
1070
|
+
for (const call of calls) {
|
|
1071
|
+
validateCallValue(call.value);
|
|
1072
|
+
if (call.gasLimit <= 0n) {
|
|
1073
|
+
throw new Error("call.gasLimit must be > 0");
|
|
1074
|
+
}
|
|
1075
|
+
if (call.gasLimit > maxGasLimit) {
|
|
1076
|
+
throw new Error(`call.gasLimit (${call.gasLimit}) exceeds maximum (${maxGasLimit})`);
|
|
1077
|
+
}
|
|
1078
|
+
const dataBytes = call.data.startsWith("0x") ? (call.data.length - 2) / 2 : call.data.length / 2;
|
|
1079
|
+
if (dataBytes > maxDataBytes) {
|
|
1080
|
+
throw new Error(`call.data length (${dataBytes} bytes) exceeds maximum (${maxDataBytes} bytes)`);
|
|
1081
|
+
}
|
|
1082
|
+
if (!call.target.startsWith("0x") || call.target.length !== 42) {
|
|
1083
|
+
throw new Error("call.target must be valid Ethereum address");
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
function validatePrecheckInput(input, config = {}) {
|
|
1088
|
+
validatePolicyId(input.auth.policyId);
|
|
1089
|
+
validateCalls(input.calls, config);
|
|
1090
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1091
|
+
const skew = 60;
|
|
1092
|
+
if (input.auth.expiresAt <= now + skew) {
|
|
1093
|
+
throw new Error("auth.expiresAt must be > now + 60s");
|
|
1094
|
+
}
|
|
1095
|
+
if (input.auth.nonce < 0n) {
|
|
1096
|
+
throw new Error("auth.nonce must be >= 0");
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
function validateRelayInput(input, config = {}) {
|
|
1100
|
+
if (input.chainId === 0) {
|
|
1101
|
+
throw new Error("chainId cannot be 0");
|
|
1102
|
+
}
|
|
1103
|
+
validatePrecheckInput(
|
|
1104
|
+
{
|
|
1105
|
+
auth: input.auth,
|
|
1106
|
+
calls: input.calls
|
|
1107
|
+
},
|
|
1108
|
+
config
|
|
1109
|
+
);
|
|
1110
|
+
if (!input.authorizationList || input.authorizationList.length !== 1) {
|
|
1111
|
+
throw new Error("authorizationList must have exactly 1 element");
|
|
1112
|
+
}
|
|
1113
|
+
input.authorizationList[0];
|
|
1114
|
+
if (!input.sessionSig.startsWith("0x") || input.sessionSig.length !== 132) {
|
|
1115
|
+
throw new Error("sessionSig must be 65-byte hex string (0x + 130 hex chars)");
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/hooks/usePrecheck.ts
|
|
1120
|
+
function usePrecheck() {
|
|
1121
|
+
const { precheck: precheckContext } = useVolr();
|
|
1122
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1123
|
+
const [error, setError] = useState(null);
|
|
1124
|
+
const precheck = useCallback(
|
|
1125
|
+
async (input) => {
|
|
1126
|
+
try {
|
|
1127
|
+
setIsLoading(true);
|
|
1128
|
+
setError(null);
|
|
1129
|
+
validatePrecheckInput(input);
|
|
1130
|
+
return await precheckContext(input);
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
const error2 = err instanceof Error ? err : new Error("Precheck failed");
|
|
1133
|
+
setError(error2);
|
|
1134
|
+
throw error2;
|
|
1135
|
+
} finally {
|
|
1136
|
+
setIsLoading(false);
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
[precheckContext]
|
|
1140
|
+
);
|
|
1141
|
+
return {
|
|
1142
|
+
precheck,
|
|
1143
|
+
isLoading,
|
|
1144
|
+
error
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function useInternalAuth() {
|
|
1148
|
+
const context = useContext(InternalAuthContext);
|
|
1149
|
+
if (!context) {
|
|
1150
|
+
throw new Error("useInternalAuth must be used within VolrProvider");
|
|
1151
|
+
}
|
|
1152
|
+
return context;
|
|
1153
|
+
}
|
|
1154
|
+
function useRelay() {
|
|
1155
|
+
const { relay: relayContext } = useVolr();
|
|
1156
|
+
const { client } = useInternalAuth();
|
|
1157
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1158
|
+
const [error, setError] = useState(null);
|
|
1159
|
+
const relay = useCallback(
|
|
1160
|
+
async (input, opts) => {
|
|
1161
|
+
if (!opts?.signer) {
|
|
1162
|
+
throw new Error("signer is required");
|
|
1163
|
+
}
|
|
1164
|
+
try {
|
|
1165
|
+
setIsLoading(true);
|
|
1166
|
+
setError(null);
|
|
1167
|
+
if (input.chainId === 0) {
|
|
1168
|
+
throw new Error("chainId cannot be 0");
|
|
1169
|
+
}
|
|
1170
|
+
const networkData = await client.get(
|
|
1171
|
+
`/networks/${input.chainId}`
|
|
1172
|
+
);
|
|
1173
|
+
const invokerAddress = networkData.invokerAddress;
|
|
1174
|
+
if (!invokerAddress) {
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
`Invoker address not configured for chainId ${input.chainId}`
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
const { sessionSig } = await signSession({
|
|
1180
|
+
signer: opts.signer,
|
|
1181
|
+
from: input.from,
|
|
1182
|
+
auth: input.auth,
|
|
1183
|
+
calls: input.calls,
|
|
1184
|
+
invokerAddress: input.from
|
|
1185
|
+
});
|
|
1186
|
+
if (!opts.rpcClient) {
|
|
1187
|
+
throw new Error("rpcClient is required for relay");
|
|
1188
|
+
}
|
|
1189
|
+
const authNonce = await getAuthNonce(
|
|
1190
|
+
opts.rpcClient,
|
|
1191
|
+
input.from,
|
|
1192
|
+
"sponsored"
|
|
1193
|
+
);
|
|
1194
|
+
const authorizationTuple = await signAuthorization({
|
|
1195
|
+
signer: opts.signer,
|
|
1196
|
+
chainId: input.chainId,
|
|
1197
|
+
address: invokerAddress,
|
|
1198
|
+
nonce: authNonce
|
|
1199
|
+
});
|
|
1200
|
+
if (authorizationTuple.address.toLowerCase() !== invokerAddress.toLowerCase()) {
|
|
1201
|
+
throw new Error(
|
|
1202
|
+
`Authorization tuple address mismatch: expected ${invokerAddress}, got ${authorizationTuple.address}`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
const relayInput = {
|
|
1206
|
+
...input,
|
|
1207
|
+
sessionSig,
|
|
1208
|
+
authorizationList: [authorizationTuple]
|
|
1209
|
+
};
|
|
1210
|
+
validateRelayInput(relayInput);
|
|
1211
|
+
return await relayContext(relayInput, {
|
|
1212
|
+
idempotencyKey: opts.idempotencyKey
|
|
1213
|
+
});
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
const error2 = err instanceof Error ? err : new Error("Relay failed");
|
|
1216
|
+
setError(error2);
|
|
1217
|
+
throw error2;
|
|
1218
|
+
} finally {
|
|
1219
|
+
setIsLoading(false);
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
[relayContext, client]
|
|
1223
|
+
);
|
|
1224
|
+
return {
|
|
1225
|
+
relay,
|
|
1226
|
+
isLoading,
|
|
1227
|
+
error
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/utils/normalize.ts
|
|
1232
|
+
function normalizeHex(value) {
|
|
1233
|
+
const hex = value.startsWith("0x") ? value.slice(2) : value;
|
|
1234
|
+
return `0x${hex.toLowerCase()}`;
|
|
1235
|
+
}
|
|
1236
|
+
function normalizeHexArray(values) {
|
|
1237
|
+
return values.map(normalizeHex);
|
|
1238
|
+
}
|
|
1239
|
+
function buildCall(options) {
|
|
1240
|
+
const data = encodeFunctionData({
|
|
1241
|
+
abi: options.abi,
|
|
1242
|
+
functionName: options.functionName,
|
|
1243
|
+
args: options.args
|
|
1244
|
+
});
|
|
1245
|
+
if (options.estimateGas) ;
|
|
1246
|
+
return {
|
|
1247
|
+
target: options.target,
|
|
1248
|
+
data,
|
|
1249
|
+
value: options.value ?? BigInt(0),
|
|
1250
|
+
gasLimit: options.gasLimit ?? BigInt(0)
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function buildCalls(options) {
|
|
1254
|
+
return options.map(buildCall);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// src/utils/rpc.ts
|
|
1258
|
+
function createExtendedRPCClient(publicClient) {
|
|
1259
|
+
return {
|
|
1260
|
+
call: async (args) => {
|
|
1261
|
+
const result = await publicClient.call({
|
|
1262
|
+
to: args.to,
|
|
1263
|
+
data: args.data
|
|
1264
|
+
});
|
|
1265
|
+
return result?.data || "0x";
|
|
1266
|
+
},
|
|
1267
|
+
getTransactionCount: async (address, blockTag) => {
|
|
1268
|
+
const count = await publicClient.getTransactionCount({
|
|
1269
|
+
address,
|
|
1270
|
+
blockTag: blockTag || "pending"
|
|
1271
|
+
});
|
|
1272
|
+
return BigInt(count);
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/utils/network.ts
|
|
1278
|
+
var networkCache = /* @__PURE__ */ new Map();
|
|
1279
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1280
|
+
function createGetRpcUrl(deps) {
|
|
1281
|
+
const { client, rpcOverrides } = deps;
|
|
1282
|
+
return async function getRpcUrl(chainId) {
|
|
1283
|
+
const overrideUrl = rpcOverrides?.[chainId.toString()];
|
|
1284
|
+
if (overrideUrl) {
|
|
1285
|
+
return overrideUrl;
|
|
1286
|
+
}
|
|
1287
|
+
const cached = networkCache.get(chainId);
|
|
1288
|
+
if (cached && cached.rpcUrl && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
1289
|
+
return cached.rpcUrl;
|
|
1290
|
+
}
|
|
1291
|
+
const response = await client.get(`/networks/${chainId}?includeRpcUrl=true`);
|
|
1292
|
+
if (!response.rpcUrl) {
|
|
1293
|
+
throw new Error(
|
|
1294
|
+
`RPC URL not available for chainId ${chainId}. Please provide it in config.rpcOverrides[${chainId}]`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
networkCache.set(chainId, {
|
|
1298
|
+
rpcUrl: response.rpcUrl,
|
|
1299
|
+
name: response.name,
|
|
1300
|
+
timestamp: Date.now()
|
|
1301
|
+
});
|
|
1302
|
+
return response.rpcUrl;
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
function createGetNetworkInfo(deps) {
|
|
1306
|
+
const { client } = deps;
|
|
1307
|
+
return async function getNetworkInfo(chainId, includeRpcUrl = false) {
|
|
1308
|
+
const cached = networkCache.get(chainId);
|
|
1309
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
1310
|
+
return {
|
|
1311
|
+
name: cached.name || `Chain ${chainId}`,
|
|
1312
|
+
rpcUrl: includeRpcUrl ? cached.rpcUrl : void 0
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
const response = await client.get(`/networks/${chainId}${includeRpcUrl ? "?includeRpcUrl=true" : ""}`);
|
|
1316
|
+
networkCache.set(chainId, {
|
|
1317
|
+
name: response.name,
|
|
1318
|
+
rpcUrl: response.rpcUrl,
|
|
1319
|
+
timestamp: Date.now()
|
|
1320
|
+
});
|
|
1321
|
+
return {
|
|
1322
|
+
name: response.name,
|
|
1323
|
+
rpcUrl: includeRpcUrl ? response.rpcUrl : void 0
|
|
1324
|
+
};
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function validatePolicyId2(policyId) {
|
|
1328
|
+
if (!policyId) {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (!policyId.startsWith("0x")) {
|
|
1332
|
+
throw new Error("policyId must start with 0x");
|
|
1333
|
+
}
|
|
1334
|
+
const hexPart = policyId.slice(2);
|
|
1335
|
+
if (hexPart.length !== 64) {
|
|
1336
|
+
throw new Error(`policyId must be 32 bytes (64 hex chars), got ${hexPart.length / 2} bytes`);
|
|
1337
|
+
}
|
|
1338
|
+
if (!/^[0-9a-f]+$/i.test(hexPart)) {
|
|
1339
|
+
throw new Error("policyId must be valid hex string");
|
|
1340
|
+
}
|
|
1341
|
+
if (policyId.toLowerCase() === ZERO_HASH.toLowerCase()) {
|
|
1342
|
+
throw new Error("Zero policyId is not allowed. Omit policyId to use project-specific policyId from backend.");
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
function validateCalls2(calls) {
|
|
1346
|
+
if (calls.length === 0) {
|
|
1347
|
+
throw new Error("calls array must not be empty");
|
|
1348
|
+
}
|
|
1349
|
+
if (calls.length > 8) {
|
|
1350
|
+
throw new Error("calls array must not exceed 8 items");
|
|
1351
|
+
}
|
|
1352
|
+
for (const call of calls) {
|
|
1353
|
+
if (call.value > 0n) {
|
|
1354
|
+
console.warn("Call has non-zero value. Most policies forbid ETH transfers.");
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function defaultGasLimit(data) {
|
|
1359
|
+
const normalized = normalizeHex(data);
|
|
1360
|
+
return normalized === "0x" || normalized === "0x0" ? 21000n : 100000n;
|
|
1361
|
+
}
|
|
1362
|
+
function normalizeCall(call) {
|
|
1363
|
+
const normalizedTarget = getAddress(call.target);
|
|
1364
|
+
const normalizedData = normalizeHex(call.data);
|
|
1365
|
+
const value = call.value ?? 0n;
|
|
1366
|
+
const gasLimit = call.gasLimit && call.gasLimit > 0n ? call.gasLimit : defaultGasLimit(normalizedData);
|
|
1367
|
+
return {
|
|
1368
|
+
target: normalizedTarget,
|
|
1369
|
+
data: normalizedData,
|
|
1370
|
+
value,
|
|
1371
|
+
gasLimit
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
function normalizeCalls(calls) {
|
|
1375
|
+
return calls.map(normalizeCall);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// src/wallet/preflight.ts
|
|
1379
|
+
function extractErrorMessage(err) {
|
|
1380
|
+
if (err && typeof err === "object") {
|
|
1381
|
+
const e = err;
|
|
1382
|
+
const parts = [];
|
|
1383
|
+
if (typeof e.shortMessage === "string") parts.push(e.shortMessage);
|
|
1384
|
+
if (typeof e.message === "string") parts.push(e.message);
|
|
1385
|
+
if (typeof e.cause?.message === "string") parts.push(e.cause.message);
|
|
1386
|
+
if (typeof e.details === "string") parts.push(e.details);
|
|
1387
|
+
const msg = parts.filter(Boolean).join(" | ");
|
|
1388
|
+
if (msg) return msg;
|
|
1389
|
+
}
|
|
1390
|
+
try {
|
|
1391
|
+
return String(err);
|
|
1392
|
+
} catch {
|
|
1393
|
+
return "Unknown error";
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
function isIgnorableFundingError(err, message) {
|
|
1397
|
+
const m = message.toLowerCase();
|
|
1398
|
+
const e = err;
|
|
1399
|
+
const name = typeof e?.name === "string" ? e.name.toLowerCase() : "";
|
|
1400
|
+
if (name === "insufficientfundserror" || name.includes("insufficientfunds")) {
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
if (typeof e?.code === "string") {
|
|
1404
|
+
const code = e.code.toLowerCase();
|
|
1405
|
+
if (code.includes("execution_reverted") || code.includes("call_exception")) {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
if (code.includes("insufficient_funds")) {
|
|
1409
|
+
return true;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
const fundingPatterns = [
|
|
1413
|
+
"insufficient funds for gas * price",
|
|
1414
|
+
"insufficient funds for gas",
|
|
1415
|
+
"for gas * price + value",
|
|
1416
|
+
"not enough funds for l1 fee",
|
|
1417
|
+
"insufficient funds for l1 fee",
|
|
1418
|
+
"insufficient balance for gas",
|
|
1419
|
+
"account balance too low"
|
|
1420
|
+
];
|
|
1421
|
+
const hasFundingPattern = fundingPatterns.some((pattern) => m.includes(pattern));
|
|
1422
|
+
const hasRevertIndicator = m.includes("revert") || m.includes("execution reverted") || m.includes("call exception") || m.includes("vm execution error");
|
|
1423
|
+
return hasFundingPattern && !hasRevertIndicator;
|
|
1424
|
+
}
|
|
1425
|
+
async function preflightEstimate(publicClient, from, calls, opts) {
|
|
1426
|
+
for (let i = 0; i < calls.length; i++) {
|
|
1427
|
+
const c = calls[i];
|
|
1428
|
+
try {
|
|
1429
|
+
await publicClient.estimateGas({
|
|
1430
|
+
account: from,
|
|
1431
|
+
to: c.target,
|
|
1432
|
+
data: c.data,
|
|
1433
|
+
value: c.value ?? 0n
|
|
1434
|
+
});
|
|
1435
|
+
} catch (e) {
|
|
1436
|
+
const message = extractErrorMessage(e);
|
|
1437
|
+
const tolerateFunding = opts?.tolerateFundingErrors !== false;
|
|
1438
|
+
const isFundingError = isIgnorableFundingError(e, message);
|
|
1439
|
+
if (tolerateFunding && isFundingError) {
|
|
1440
|
+
try {
|
|
1441
|
+
await publicClient.call({
|
|
1442
|
+
account: from,
|
|
1443
|
+
to: c.target,
|
|
1444
|
+
data: c.data,
|
|
1445
|
+
value: c.value ?? 0n
|
|
1446
|
+
});
|
|
1447
|
+
console.log(
|
|
1448
|
+
`[preflightEstimate] Ignoring funding error for call #${i} (target ${c.target}): ${message}`
|
|
1449
|
+
);
|
|
1450
|
+
continue;
|
|
1451
|
+
} catch (callError) {
|
|
1452
|
+
const callMessage = extractErrorMessage(callError);
|
|
1453
|
+
console.error(
|
|
1454
|
+
`[preflightEstimate] Static call after funding error also failed for call #${i} (target ${c.target}):`,
|
|
1455
|
+
{
|
|
1456
|
+
originalError: { message, errorName: e?.name, errorCode: e?.code },
|
|
1457
|
+
callError: {
|
|
1458
|
+
message: callMessage,
|
|
1459
|
+
name: callError?.name,
|
|
1460
|
+
code: callError?.code
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
);
|
|
1464
|
+
throw new Error(
|
|
1465
|
+
`Preflight failed (call #${i} target ${c.target}): ${callMessage}`
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
console.error(
|
|
1470
|
+
`[preflightEstimate] Preflight failed for call #${i} (target ${c.target}):`,
|
|
1471
|
+
{
|
|
1472
|
+
message,
|
|
1473
|
+
errorName: e?.name,
|
|
1474
|
+
errorCode: e?.code,
|
|
1475
|
+
isFundingError,
|
|
1476
|
+
tolerateFunding
|
|
1477
|
+
}
|
|
1478
|
+
);
|
|
1479
|
+
throw new Error(`Preflight failed (call #${i} target ${c.target}): ${message}`);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
async function resolveSigner(input) {
|
|
1484
|
+
const { explicitSigner, provider, chainId, client, user, setProvider } = input;
|
|
1485
|
+
if (explicitSigner) {
|
|
1486
|
+
return { signer: explicitSigner, activeProvider: null };
|
|
1487
|
+
}
|
|
1488
|
+
if (provider) {
|
|
1489
|
+
await provider.ensureSession({ interactive: true, force: true });
|
|
1490
|
+
const signerContext = {
|
|
1491
|
+
provider,
|
|
1492
|
+
chainId,
|
|
1493
|
+
// Citrea testnet (5115) doesn't support P-256
|
|
1494
|
+
p256Hint: false
|
|
1495
|
+
};
|
|
1496
|
+
const signer = await selectSigner(signerContext);
|
|
1497
|
+
return { signer, activeProvider: provider };
|
|
1498
|
+
}
|
|
1499
|
+
if (user?.keyStorageType === "passkey" && user.blobUrl && user.prfInput && user.id) {
|
|
1500
|
+
const { provider: restoredProvider } = await restorePasskey({
|
|
1501
|
+
client,
|
|
1502
|
+
userId: user.id,
|
|
1503
|
+
blobUrl: user.blobUrl,
|
|
1504
|
+
prfInput: user.prfInput,
|
|
1505
|
+
credentialId: user.credentialId
|
|
1506
|
+
});
|
|
1507
|
+
await setProvider(restoredProvider);
|
|
1508
|
+
await restoredProvider.ensureSession({ interactive: true, force: true });
|
|
1509
|
+
const signerContext = {
|
|
1510
|
+
provider: restoredProvider,
|
|
1511
|
+
chainId,
|
|
1512
|
+
p256Hint: false
|
|
1513
|
+
};
|
|
1514
|
+
const signer = await selectSigner(signerContext);
|
|
1515
|
+
return { signer, activeProvider: restoredProvider };
|
|
1516
|
+
}
|
|
1517
|
+
throw new Error(
|
|
1518
|
+
"No wallet provider configured. Please set up a Passkey provider to sign transactions. SIWE is authentication-only."
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/utils/defaults.ts
|
|
1523
|
+
var DEFAULT_EXPIRES_IN_SEC = 900;
|
|
1524
|
+
var DEFAULT_MODE = "sponsored";
|
|
1525
|
+
function defaultIdempotencyKey() {
|
|
1526
|
+
const c = globalThis.crypto;
|
|
1527
|
+
return c?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/wallet/auth.ts
|
|
1531
|
+
var GAS_CAP_BUFFER = 150000n;
|
|
1532
|
+
function computeTotalGasCap(calls) {
|
|
1533
|
+
return calls.reduce((sum, c) => sum + (c.gasLimit ?? 0n), 0n) + GAS_CAP_BUFFER;
|
|
1534
|
+
}
|
|
1535
|
+
function buildTempAuth(input) {
|
|
1536
|
+
const expiresAt = Math.floor(Date.now() / 1e3) + (input.expiresInSec ?? DEFAULT_EXPIRES_IN_SEC);
|
|
1537
|
+
const totalGasCap = computeTotalGasCap(input.calls);
|
|
1538
|
+
const sessionId = input.sessionId ?? 0n;
|
|
1539
|
+
const policySnapshotHash = input.policySnapshotHash ?? ZERO_HASH;
|
|
1540
|
+
const gasLimitMax = input.gasLimitMax ?? totalGasCap;
|
|
1541
|
+
const maxFeePerGas = input.maxFeePerGas ?? 1000000000n;
|
|
1542
|
+
const maxPriorityFeePerGas = input.maxPriorityFeePerGas ?? 1000000n;
|
|
1543
|
+
return {
|
|
1544
|
+
chainId: input.chainId,
|
|
1545
|
+
sessionKey: input.from,
|
|
1546
|
+
sessionId,
|
|
1547
|
+
expiresAt,
|
|
1548
|
+
policyId: normalizeHex(input.policyId),
|
|
1549
|
+
nonce: 0n,
|
|
1550
|
+
policySnapshotHash,
|
|
1551
|
+
gasLimitMax,
|
|
1552
|
+
maxFeePerGas,
|
|
1553
|
+
maxPriorityFeePerGas,
|
|
1554
|
+
totalGasCap
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
function finalizeAuthWithNonce(tempAuth, quote) {
|
|
1558
|
+
let finalNonce;
|
|
1559
|
+
if (quote?.currentOpNonce !== void 0 && quote.currentOpNonce !== null && quote.currentOpNonce !== "") {
|
|
1560
|
+
const currentNonce = BigInt(quote.currentOpNonce);
|
|
1561
|
+
finalNonce = currentNonce + 1n;
|
|
1562
|
+
} else {
|
|
1563
|
+
finalNonce = BigInt(Math.floor(Date.now() / 1e3));
|
|
1564
|
+
}
|
|
1565
|
+
const finalAuth = {
|
|
1566
|
+
...tempAuth,
|
|
1567
|
+
nonce: finalNonce
|
|
1568
|
+
};
|
|
1569
|
+
if (quote?.policyId) {
|
|
1570
|
+
finalAuth.policyId = normalizeHex(quote.policyId);
|
|
1571
|
+
}
|
|
1572
|
+
if (quote?.policySnapshotHash) {
|
|
1573
|
+
finalAuth.policySnapshotHash = quote.policySnapshotHash;
|
|
1574
|
+
}
|
|
1575
|
+
return finalAuth;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// src/utils/tx-polling.ts
|
|
1579
|
+
async function pollTransactionStatus(txId, client, maxAttempts = 60, intervalMs = 5e3) {
|
|
1580
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1581
|
+
try {
|
|
1582
|
+
const response = await client.get(`/wallet/transactions/${txId}`);
|
|
1583
|
+
const { status, txHash } = response;
|
|
1584
|
+
if (status === "CONFIRMED") {
|
|
1585
|
+
console.log(`[pollTransactionStatus] Transaction ${txId} confirmed with txHash ${txHash}`);
|
|
1586
|
+
return {
|
|
1587
|
+
txId,
|
|
1588
|
+
status: "CONFIRMED",
|
|
1589
|
+
txHash
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
if (status === "FAILED") {
|
|
1593
|
+
console.error(`[pollTransactionStatus] Transaction ${txId} failed`);
|
|
1594
|
+
const diag = response.meta?.developerDiagnostics;
|
|
1595
|
+
if (diag) {
|
|
1596
|
+
console.error("[volr][relay] developerDiagnostics:", diag);
|
|
1597
|
+
}
|
|
1598
|
+
return {
|
|
1599
|
+
txId,
|
|
1600
|
+
status: "FAILED",
|
|
1601
|
+
txHash
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
console.log(`[pollTransactionStatus] Transaction ${txId} is ${status}, attempt ${attempt + 1}/${maxAttempts}`);
|
|
1605
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
console.error(`[pollTransactionStatus] Error polling transaction ${txId}:`, error);
|
|
1608
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
console.warn(`[pollTransactionStatus] Polling timeout for transaction ${txId}`);
|
|
1612
|
+
return {
|
|
1613
|
+
txId,
|
|
1614
|
+
status: "PENDING"
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// src/wallet/sender.ts
|
|
1619
|
+
async function sendCalls(args) {
|
|
1620
|
+
const { chainId, from, calls, opts, deps } = args;
|
|
1621
|
+
if (chainId === 0) {
|
|
1622
|
+
throw new Error("chainId cannot be 0");
|
|
1623
|
+
}
|
|
1624
|
+
const effectivePolicyId = opts.policyId?.toLowerCase() === ZERO_HASH.toLowerCase() ? void 0 : opts.policyId;
|
|
1625
|
+
validatePolicyId2(effectivePolicyId);
|
|
1626
|
+
const normalizedFrom = from;
|
|
1627
|
+
const normalizedCalls = normalizeCalls(calls);
|
|
1628
|
+
validateCalls2(normalizedCalls);
|
|
1629
|
+
let currentUser = deps.user;
|
|
1630
|
+
if (deps.user?.keyStorageType === "passkey" && !deps.provider) {
|
|
1631
|
+
try {
|
|
1632
|
+
const refreshResponse = await deps.client.post("/auth/refresh", {});
|
|
1633
|
+
if (refreshResponse.user) {
|
|
1634
|
+
currentUser = refreshResponse.user;
|
|
1635
|
+
}
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
console.warn("[sendCalls] Failed to refresh user data before transaction:", error);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (opts.preflight !== false) {
|
|
1641
|
+
const tolerateFundingErrors = (opts.mode ?? "sponsored") !== "self";
|
|
1642
|
+
await preflightEstimate(deps.publicClient, normalizedFrom, normalizedCalls, { tolerateFundingErrors });
|
|
1643
|
+
}
|
|
1644
|
+
const { signer, activeProvider } = await resolveSigner({
|
|
1645
|
+
explicitSigner: opts.signer,
|
|
1646
|
+
provider: deps.provider,
|
|
1647
|
+
chainId,
|
|
1648
|
+
client: deps.client,
|
|
1649
|
+
user: currentUser,
|
|
1650
|
+
setProvider: deps.setProvider
|
|
1651
|
+
});
|
|
1652
|
+
let projectPolicyId;
|
|
1653
|
+
if (!effectivePolicyId) {
|
|
1654
|
+
const tempAuthForPrecheck = buildTempAuth({
|
|
1655
|
+
chainId,
|
|
1656
|
+
from: normalizedFrom,
|
|
1657
|
+
policyId: ZERO_HASH,
|
|
1658
|
+
calls: normalizedCalls,
|
|
1659
|
+
expiresInSec: opts.expiresInSec ?? DEFAULT_EXPIRES_IN_SEC
|
|
1660
|
+
});
|
|
1661
|
+
let precheckQuote;
|
|
1662
|
+
try {
|
|
1663
|
+
precheckQuote = await deps.precheck({ auth: tempAuthForPrecheck, calls: normalizedCalls });
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
throw new Error(`Precheck failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1666
|
+
}
|
|
1667
|
+
const quotePolicyId = precheckQuote.policyId;
|
|
1668
|
+
if (!quotePolicyId) {
|
|
1669
|
+
throw new Error("Backend did not return policyId in precheck response");
|
|
1670
|
+
}
|
|
1671
|
+
projectPolicyId = quotePolicyId;
|
|
1672
|
+
} else {
|
|
1673
|
+
projectPolicyId = effectivePolicyId;
|
|
1674
|
+
}
|
|
1675
|
+
const tempAuth = buildTempAuth({
|
|
1676
|
+
chainId,
|
|
1677
|
+
from: normalizedFrom,
|
|
1678
|
+
policyId: projectPolicyId,
|
|
1679
|
+
calls: normalizedCalls,
|
|
1680
|
+
expiresInSec: opts.expiresInSec ?? DEFAULT_EXPIRES_IN_SEC
|
|
1681
|
+
});
|
|
1682
|
+
let quote;
|
|
1683
|
+
try {
|
|
1684
|
+
quote = await deps.precheck({ auth: tempAuth, calls: normalizedCalls });
|
|
1685
|
+
} catch (err) {
|
|
1686
|
+
console.warn("[useVolrWallet] Precheck failed:", err);
|
|
1687
|
+
}
|
|
1688
|
+
const auth = finalizeAuthWithNonce(tempAuth, quote);
|
|
1689
|
+
const idempotencyKey = opts.idempotencyKey ?? defaultIdempotencyKey();
|
|
1690
|
+
try {
|
|
1691
|
+
const result = await deps.relay(
|
|
1692
|
+
{ chainId, from: normalizedFrom, auth, calls: normalizedCalls },
|
|
1693
|
+
{ signer, rpcClient: deps.rpcClient, idempotencyKey }
|
|
1694
|
+
);
|
|
1695
|
+
if (result.status === "QUEUED" || result.status === "PENDING") {
|
|
1696
|
+
console.log(`[useVolrWallet] Transaction ${result.txId} is ${result.status}, starting polling...`);
|
|
1697
|
+
return await pollTransactionStatus(result.txId, deps.client);
|
|
1698
|
+
}
|
|
1699
|
+
return result;
|
|
1700
|
+
} finally {
|
|
1701
|
+
if (activeProvider && activeProvider.lock) {
|
|
1702
|
+
try {
|
|
1703
|
+
await activeProvider.lock();
|
|
1704
|
+
} catch {
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/hooks/useVolrWallet.ts
|
|
1711
|
+
function useVolrWallet() {
|
|
1712
|
+
const { user, config, provider, setProvider } = useVolr();
|
|
1713
|
+
const { precheck } = usePrecheck();
|
|
1714
|
+
const { relay } = useRelay();
|
|
1715
|
+
const { client } = useInternalAuth();
|
|
1716
|
+
const getRpcUrl = useCallback(
|
|
1717
|
+
createGetRpcUrl({ client, rpcOverrides: config.rpcOverrides }),
|
|
1718
|
+
[client, config.rpcOverrides]
|
|
1719
|
+
);
|
|
1720
|
+
const evm = useCallback(
|
|
1721
|
+
(chainId) => {
|
|
1722
|
+
if (chainId === 0) {
|
|
1723
|
+
throw new Error("chainId cannot be 0");
|
|
1724
|
+
}
|
|
1725
|
+
let publicClient = null;
|
|
1726
|
+
let extendedRpcClient = null;
|
|
1727
|
+
const ensureRpcClient = async () => {
|
|
1728
|
+
if (!publicClient) {
|
|
1729
|
+
const rpcUrl = await getRpcUrl(chainId);
|
|
1730
|
+
publicClient = createPublicClient({
|
|
1731
|
+
transport: http(rpcUrl)
|
|
1732
|
+
});
|
|
1733
|
+
extendedRpcClient = createExtendedRPCClient(publicClient);
|
|
1734
|
+
}
|
|
1735
|
+
return { publicClient, extendedRpcClient };
|
|
1736
|
+
};
|
|
1737
|
+
return {
|
|
1738
|
+
readContract: async (args) => {
|
|
1739
|
+
const { publicClient: client2 } = await ensureRpcClient();
|
|
1740
|
+
return client2.readContract(args);
|
|
1741
|
+
},
|
|
1742
|
+
sendTransaction: async (tx, opts) => {
|
|
1743
|
+
const { publicClient: publicClient2, extendedRpcClient: rpcClient } = await ensureRpcClient();
|
|
1744
|
+
const from = opts.from ?? user?.evmAddress;
|
|
1745
|
+
if (!from) {
|
|
1746
|
+
throw new Error(
|
|
1747
|
+
"from address is required. Provide it in opts.from or set user.evmAddress in VolrProvider. If you haven't set up a wallet provider yet, please complete the onboarding flow."
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
const call = {
|
|
1751
|
+
target: getAddress(tx.to),
|
|
1752
|
+
data: normalizeHex(tx.data),
|
|
1753
|
+
value: tx.value ?? 0n,
|
|
1754
|
+
gasLimit: tx.gasLimit ?? 0n
|
|
1755
|
+
};
|
|
1756
|
+
return await sendCalls({
|
|
1757
|
+
chainId,
|
|
1758
|
+
from: getAddress(from),
|
|
1759
|
+
calls: [call],
|
|
1760
|
+
opts,
|
|
1761
|
+
deps: {
|
|
1762
|
+
publicClient: publicClient2,
|
|
1763
|
+
rpcClient,
|
|
1764
|
+
precheck,
|
|
1765
|
+
relay,
|
|
1766
|
+
client,
|
|
1767
|
+
user: user ?? null,
|
|
1768
|
+
provider: provider ?? null,
|
|
1769
|
+
setProvider
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
},
|
|
1773
|
+
sendBatch: (async (calls, opts) => {
|
|
1774
|
+
const { publicClient: publicClient2, extendedRpcClient: rpcClient } = await ensureRpcClient();
|
|
1775
|
+
const from = opts.from ?? user?.evmAddress;
|
|
1776
|
+
if (!from) {
|
|
1777
|
+
throw new Error(
|
|
1778
|
+
"from address is required. Provide it in opts.from or set user.evmAddress in VolrProvider. If you haven't set up a wallet provider yet, please complete the onboarding flow."
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
const isCallArray = (calls2) => {
|
|
1782
|
+
return Array.isArray(calls2) && calls2.length > 0 && "data" in calls2[0] && !("abi" in calls2[0]);
|
|
1783
|
+
};
|
|
1784
|
+
const builtCalls = isCallArray(calls) ? calls : buildCalls(calls);
|
|
1785
|
+
return await sendCalls({
|
|
1786
|
+
chainId,
|
|
1787
|
+
from: getAddress(from),
|
|
1788
|
+
calls: builtCalls.map((c) => ({
|
|
1789
|
+
target: getAddress(c.target),
|
|
1790
|
+
data: normalizeHex(c.data),
|
|
1791
|
+
value: c.value ?? 0n,
|
|
1792
|
+
gasLimit: c.gasLimit ?? 0n
|
|
1793
|
+
})),
|
|
1794
|
+
opts,
|
|
1795
|
+
deps: {
|
|
1796
|
+
publicClient: publicClient2,
|
|
1797
|
+
rpcClient,
|
|
1798
|
+
precheck,
|
|
1799
|
+
relay,
|
|
1800
|
+
client,
|
|
1801
|
+
user: user ?? null,
|
|
1802
|
+
provider: provider ?? null,
|
|
1803
|
+
setProvider
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
})
|
|
1807
|
+
};
|
|
1808
|
+
},
|
|
1809
|
+
[user, config, provider, precheck, relay, getRpcUrl]
|
|
1810
|
+
);
|
|
1811
|
+
return { evm };
|
|
1812
|
+
}
|
|
1813
|
+
function createAxiosInstance(baseUrl, apiKey) {
|
|
1814
|
+
const instance = axios.create({
|
|
1815
|
+
baseURL: baseUrl.replace(/\/+$/, ""),
|
|
1816
|
+
// Remove trailing slashes
|
|
1817
|
+
withCredentials: true,
|
|
1818
|
+
// Include cookies
|
|
1819
|
+
headers: {
|
|
1820
|
+
"Content-Type": "application/json"
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
instance.interceptors.request.use((config) => {
|
|
1824
|
+
if (apiKey) {
|
|
1825
|
+
config.headers["X-API-Key"] = apiKey;
|
|
1826
|
+
}
|
|
1827
|
+
return config;
|
|
1828
|
+
});
|
|
1829
|
+
instance.interceptors.response.use(
|
|
1830
|
+
(response) => response,
|
|
1831
|
+
(error) => {
|
|
1832
|
+
if (error.response?.data) {
|
|
1833
|
+
const errorData = error.response.data;
|
|
1834
|
+
if (errorData.error?.message) {
|
|
1835
|
+
error.message = errorData.error.message;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return Promise.reject(error);
|
|
1839
|
+
}
|
|
1840
|
+
);
|
|
1841
|
+
return instance;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/hooks/useVolrLogin.ts
|
|
1845
|
+
function useVolrLogin() {
|
|
1846
|
+
const { config, setUser } = useVolr();
|
|
1847
|
+
const { refreshAccessToken, setAccessToken } = useInternalAuth();
|
|
1848
|
+
const toVolrUser = useCallback((u) => {
|
|
1849
|
+
return {
|
|
1850
|
+
id: u.id,
|
|
1851
|
+
email: u.email,
|
|
1852
|
+
accountId: u.accountId ?? void 0,
|
|
1853
|
+
evmAddress: u.evmAddress,
|
|
1854
|
+
keyStorageType: u.keyStorageType ?? void 0,
|
|
1855
|
+
signerType: u.signerType ?? void 0,
|
|
1856
|
+
walletConnector: u.walletConnector ?? void 0,
|
|
1857
|
+
lastWalletChainId: u.lastWalletChainId ?? void 0,
|
|
1858
|
+
blobUrl: u.blobUrl ?? void 0,
|
|
1859
|
+
prfInput: u.prfInput ?? void 0,
|
|
1860
|
+
credentialId: u.credentialId ?? void 0
|
|
1861
|
+
};
|
|
1862
|
+
}, []);
|
|
1863
|
+
const api = useMemo(
|
|
1864
|
+
() => createAxiosInstance(config.apiBaseUrl, config.projectApiKey),
|
|
1865
|
+
[config.apiBaseUrl, config.projectApiKey]
|
|
1866
|
+
);
|
|
1867
|
+
const requestEmailCode = useCallback(
|
|
1868
|
+
async (email) => {
|
|
1869
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
1870
|
+
if (!normalizedEmail || !normalizedEmail.includes("@")) {
|
|
1871
|
+
throw new Error("Invalid email address");
|
|
1872
|
+
}
|
|
1873
|
+
const response = await api.post("/auth/email/send", {
|
|
1874
|
+
email: normalizedEmail
|
|
1875
|
+
});
|
|
1876
|
+
if (!response.data?.ok) {
|
|
1877
|
+
throw new Error(
|
|
1878
|
+
response.data?.error?.message || "Failed to send verification code"
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
safeStorage.setItem(STORAGE_KEYS.lastEmail, normalizedEmail);
|
|
1882
|
+
},
|
|
1883
|
+
[api]
|
|
1884
|
+
);
|
|
1885
|
+
const verifyEmailCode = useCallback(
|
|
1886
|
+
async (email, code) => {
|
|
1887
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
1888
|
+
if (!normalizedEmail || !normalizedEmail.includes("@")) {
|
|
1889
|
+
throw new Error("Invalid email address");
|
|
1890
|
+
}
|
|
1891
|
+
const normalizedCode = code.trim();
|
|
1892
|
+
if (!/^\d{6}$/.test(normalizedCode)) {
|
|
1893
|
+
throw new Error("Invalid code format");
|
|
1894
|
+
}
|
|
1895
|
+
const response = await api.post("/auth/email/verify", {
|
|
1896
|
+
email: normalizedEmail,
|
|
1897
|
+
code: normalizedCode
|
|
1898
|
+
});
|
|
1899
|
+
if (!response.data?.ok) {
|
|
1900
|
+
throw new Error(
|
|
1901
|
+
response.data?.error?.message || "Invalid verification code"
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
const verifyData = response.data;
|
|
1905
|
+
const userFromServer = verifyData?.data?.user;
|
|
1906
|
+
const isNewUser = !!verifyData?.data?.isNewUser;
|
|
1907
|
+
const accessToken = verifyData?.data?.accessToken || "";
|
|
1908
|
+
if (!accessToken) {
|
|
1909
|
+
throw new Error(
|
|
1910
|
+
"Access token is required but was not provided by the server"
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
setAccessToken(accessToken);
|
|
1914
|
+
await refreshAccessToken();
|
|
1915
|
+
if (userFromServer) {
|
|
1916
|
+
setUser(toVolrUser(userFromServer));
|
|
1917
|
+
} else {
|
|
1918
|
+
setUser({ id: "", email: normalizedEmail });
|
|
1919
|
+
}
|
|
1920
|
+
return {
|
|
1921
|
+
userId: userFromServer?.id || "",
|
|
1922
|
+
isNewUser,
|
|
1923
|
+
keyStorageType: userFromServer?.keyStorageType ?? null,
|
|
1924
|
+
signerType: userFromServer?.signerType ?? null,
|
|
1925
|
+
accessToken
|
|
1926
|
+
};
|
|
1927
|
+
},
|
|
1928
|
+
[api, refreshAccessToken, setAccessToken, setUser, toVolrUser]
|
|
1929
|
+
);
|
|
1930
|
+
const handleSocialLogin = useCallback(
|
|
1931
|
+
async (provider) => {
|
|
1932
|
+
if (typeof window !== "undefined") {
|
|
1933
|
+
const baseUrl = config.apiBaseUrl.replace(/\/+$/, "");
|
|
1934
|
+
window.location.href = `${baseUrl}/auth/${provider}`;
|
|
1935
|
+
}
|
|
1936
|
+
},
|
|
1937
|
+
[config.apiBaseUrl]
|
|
1938
|
+
);
|
|
1939
|
+
const requestSiweNonce = useCallback(async () => {
|
|
1940
|
+
const response = await api.get("/auth/siwe/nonce");
|
|
1941
|
+
if (!response.data?.ok) {
|
|
1942
|
+
throw new Error(
|
|
1943
|
+
response.data?.error?.message || "Failed to generate nonce"
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
return response.data.data.nonce;
|
|
1947
|
+
}, [api]);
|
|
1948
|
+
const verifySiweSignature = useCallback(
|
|
1949
|
+
async (message, signature, options) => {
|
|
1950
|
+
try {
|
|
1951
|
+
const response = await api.post("/auth/siwe/verify", {
|
|
1952
|
+
message,
|
|
1953
|
+
signature,
|
|
1954
|
+
walletConnector: options?.walletConnector,
|
|
1955
|
+
chainId: options?.chainId
|
|
1956
|
+
});
|
|
1957
|
+
if (!response.data?.ok) {
|
|
1958
|
+
console.error(
|
|
1959
|
+
"[verifySiweSignature] Backend returned error:",
|
|
1960
|
+
response.data
|
|
1961
|
+
);
|
|
1962
|
+
throw new Error(
|
|
1963
|
+
response.data?.error?.message || "Invalid SIWE signature"
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
const verifyData = response.data;
|
|
1967
|
+
const userFromServer = verifyData?.data?.user;
|
|
1968
|
+
const isNewUser = !!verifyData?.data?.isNewUser;
|
|
1969
|
+
const accessToken = verifyData?.data?.accessToken || "";
|
|
1970
|
+
if (!accessToken) {
|
|
1971
|
+
throw new Error(
|
|
1972
|
+
"Access token is required but was not provided by the server"
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
setAccessToken(accessToken);
|
|
1976
|
+
await refreshAccessToken();
|
|
1977
|
+
if (userFromServer) {
|
|
1978
|
+
setUser(toVolrUser(userFromServer));
|
|
1979
|
+
}
|
|
1980
|
+
return {
|
|
1981
|
+
userId: userFromServer?.id || "",
|
|
1982
|
+
isNewUser,
|
|
1983
|
+
keyStorageType: userFromServer?.keyStorageType ?? null,
|
|
1984
|
+
signerType: userFromServer?.signerType ?? null,
|
|
1985
|
+
accessToken
|
|
1986
|
+
};
|
|
1987
|
+
} catch (error) {
|
|
1988
|
+
console.error("[verifySiweSignature] Error details:", {
|
|
1989
|
+
message: error.message,
|
|
1990
|
+
response: error.response?.data,
|
|
1991
|
+
status: error.response?.status
|
|
1992
|
+
});
|
|
1993
|
+
throw error;
|
|
1994
|
+
}
|
|
1995
|
+
},
|
|
1996
|
+
[api, refreshAccessToken, setAccessToken, setUser, toVolrUser]
|
|
1997
|
+
);
|
|
1998
|
+
const handlePasskeyComplete = useCallback(async () => {
|
|
1999
|
+
}, []);
|
|
2000
|
+
return {
|
|
2001
|
+
requestEmailCode,
|
|
2002
|
+
verifyEmailCode,
|
|
2003
|
+
handleSocialLogin,
|
|
2004
|
+
requestSiweNonce,
|
|
2005
|
+
verifySiweSignature,
|
|
2006
|
+
handlePasskeyComplete
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
async function jsonRpc(rpcUrl, method, params) {
|
|
2010
|
+
const payload = {
|
|
2011
|
+
jsonrpc: "2.0",
|
|
2012
|
+
id: Date.now(),
|
|
2013
|
+
method,
|
|
2014
|
+
params
|
|
2015
|
+
};
|
|
2016
|
+
const res = await fetch(rpcUrl, {
|
|
2017
|
+
method: "POST",
|
|
2018
|
+
headers: { "content-type": "application/json" },
|
|
2019
|
+
body: JSON.stringify(payload)
|
|
2020
|
+
});
|
|
2021
|
+
if (!res.ok) {
|
|
2022
|
+
throw new Error(`RPC ${method} failed: ${res.status} ${res.statusText}`);
|
|
2023
|
+
}
|
|
2024
|
+
const body = await res.json();
|
|
2025
|
+
if (body.error) {
|
|
2026
|
+
throw new Error(body.error.message || "RPC error");
|
|
2027
|
+
}
|
|
2028
|
+
return body.result;
|
|
2029
|
+
}
|
|
2030
|
+
function hexToBigInt(hex) {
|
|
2031
|
+
if (!hex) return 0n;
|
|
2032
|
+
return BigInt(hex);
|
|
2033
|
+
}
|
|
2034
|
+
function pad32(address) {
|
|
2035
|
+
return `0x${address.toLowerCase().replace(/^0x/, "").padStart(64, "0")}`;
|
|
2036
|
+
}
|
|
2037
|
+
function balanceOfCalldata(address) {
|
|
2038
|
+
const selector = "0x70a08231";
|
|
2039
|
+
return `${selector}${pad32(address).slice(2)}`;
|
|
2040
|
+
}
|
|
2041
|
+
function useDepositListener(input) {
|
|
2042
|
+
const { config } = useVolr();
|
|
2043
|
+
const { client } = useInternalAuth();
|
|
2044
|
+
const [status, setStatus] = useState({ state: "idle" });
|
|
2045
|
+
const intervalRef = useRef(null);
|
|
2046
|
+
const rpcUrlRef = useRef(null);
|
|
2047
|
+
const getRpcUrl = useCallback(
|
|
2048
|
+
createGetRpcUrl({ client, rpcOverrides: config.rpcOverrides }),
|
|
2049
|
+
[client, config.rpcOverrides]
|
|
2050
|
+
);
|
|
2051
|
+
useEffect(() => {
|
|
2052
|
+
let cancelled = false;
|
|
2053
|
+
const fetchBalance = async () => {
|
|
2054
|
+
try {
|
|
2055
|
+
if (!rpcUrlRef.current) {
|
|
2056
|
+
throw new Error("RPC URL not initialized");
|
|
2057
|
+
}
|
|
2058
|
+
const rpcUrl = rpcUrlRef.current;
|
|
2059
|
+
if (input.asset.kind === "native") {
|
|
2060
|
+
const result = await jsonRpc(rpcUrl, "eth_getBalance", [
|
|
2061
|
+
input.address,
|
|
2062
|
+
"latest"
|
|
2063
|
+
]);
|
|
2064
|
+
return hexToBigInt(result);
|
|
2065
|
+
} else {
|
|
2066
|
+
const data = balanceOfCalldata(input.address);
|
|
2067
|
+
const result = await jsonRpc(rpcUrl, "eth_call", [
|
|
2068
|
+
{ to: input.asset.token.address, data },
|
|
2069
|
+
"latest"
|
|
2070
|
+
]);
|
|
2071
|
+
return hexToBigInt(result);
|
|
2072
|
+
}
|
|
2073
|
+
} catch (err) {
|
|
2074
|
+
throw new Error(
|
|
2075
|
+
err?.message || "Failed to fetch balance from RPC endpoint"
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
const start = async () => {
|
|
2080
|
+
try {
|
|
2081
|
+
if (!rpcUrlRef.current) {
|
|
2082
|
+
rpcUrlRef.current = await getRpcUrl(input.chainId);
|
|
2083
|
+
console.log("[DepositListener] RPC URL:", rpcUrlRef.current);
|
|
2084
|
+
}
|
|
2085
|
+
const initial = await fetchBalance();
|
|
2086
|
+
console.log("[DepositListener] Initial balance:", initial.toString());
|
|
2087
|
+
if (cancelled) return;
|
|
2088
|
+
setStatus({ state: "listening", balance: initial });
|
|
2089
|
+
} catch (e) {
|
|
2090
|
+
console.error("[DepositListener] Error:", e);
|
|
2091
|
+
if (cancelled) return;
|
|
2092
|
+
setStatus({ state: "error", message: e.message || String(e) });
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
const intervalMs = input.pollIntervalMs ?? config.deposit?.pollIntervalMs ?? 6e3;
|
|
2096
|
+
console.log("[DepositListener] Polling interval:", intervalMs, "ms");
|
|
2097
|
+
intervalRef.current = window.setInterval(async () => {
|
|
2098
|
+
try {
|
|
2099
|
+
const current = await fetchBalance();
|
|
2100
|
+
console.log("[DepositListener] Current balance:", current.toString());
|
|
2101
|
+
if (cancelled) return;
|
|
2102
|
+
setStatus((prev) => {
|
|
2103
|
+
if (prev.state === "listening") {
|
|
2104
|
+
if (current > prev.balance) {
|
|
2105
|
+
console.log("[DepositListener] \u2705 DEPOSIT DETECTED! Previous:", prev.balance.toString(), "New:", current.toString());
|
|
2106
|
+
return {
|
|
2107
|
+
state: "detected",
|
|
2108
|
+
previousBalance: prev.balance,
|
|
2109
|
+
newBalance: current,
|
|
2110
|
+
delta: current - prev.balance
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
if (current !== prev.balance) {
|
|
2114
|
+
console.log("[DepositListener] \u26A0\uFE0F Balance changed (decreased or other). Previous:", prev.balance.toString(), "New:", current.toString());
|
|
2115
|
+
return {
|
|
2116
|
+
state: "detected",
|
|
2117
|
+
previousBalance: prev.balance,
|
|
2118
|
+
newBalance: current,
|
|
2119
|
+
delta: current - prev.balance
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
return prev;
|
|
2123
|
+
}
|
|
2124
|
+
if (prev.state === "detected") {
|
|
2125
|
+
console.log("[DepositListener] Resuming listening from new balance:", current.toString());
|
|
2126
|
+
return { state: "listening", balance: current };
|
|
2127
|
+
}
|
|
2128
|
+
return prev;
|
|
2129
|
+
});
|
|
2130
|
+
} catch (err) {
|
|
2131
|
+
console.error("[DepositListener] Polling error:", err);
|
|
2132
|
+
if (cancelled) return;
|
|
2133
|
+
setStatus({ state: "error", message: err.message || String(err) });
|
|
2134
|
+
}
|
|
2135
|
+
}, intervalMs);
|
|
2136
|
+
};
|
|
2137
|
+
start();
|
|
2138
|
+
return () => {
|
|
2139
|
+
cancelled = true;
|
|
2140
|
+
rpcUrlRef.current = null;
|
|
2141
|
+
if (intervalRef.current !== null) {
|
|
2142
|
+
clearInterval(intervalRef.current);
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
}, [getRpcUrl, input.chainId, input.address, JSON.stringify(input.asset)]);
|
|
2146
|
+
return status;
|
|
2147
|
+
}
|
|
2148
|
+
async function enrollPasskey(params) {
|
|
2149
|
+
const {
|
|
2150
|
+
client,
|
|
2151
|
+
baseUrl,
|
|
2152
|
+
apiKey,
|
|
2153
|
+
userId,
|
|
2154
|
+
userEmail,
|
|
2155
|
+
projectId,
|
|
2156
|
+
rpId = typeof window !== "undefined" ? window.location.hostname : "localhost",
|
|
2157
|
+
rpName = "Volr"
|
|
2158
|
+
} = params;
|
|
2159
|
+
if (!userId) {
|
|
2160
|
+
throw new Error("userId is required");
|
|
2161
|
+
}
|
|
2162
|
+
if (!userEmail) {
|
|
2163
|
+
throw new Error("userEmail is required");
|
|
2164
|
+
}
|
|
2165
|
+
if (!projectId) {
|
|
2166
|
+
throw new Error("projectId is required");
|
|
2167
|
+
}
|
|
2168
|
+
if (!baseUrl) {
|
|
2169
|
+
throw new Error("baseUrl is required");
|
|
2170
|
+
}
|
|
2171
|
+
if (!apiKey) {
|
|
2172
|
+
throw new Error("apiKey is required");
|
|
2173
|
+
}
|
|
2174
|
+
if (!navigator.credentials || !navigator.credentials.create) {
|
|
2175
|
+
throw new Error("WebAuthn API is not supported");
|
|
2176
|
+
}
|
|
2177
|
+
const challenge = new Uint8Array(32);
|
|
2178
|
+
crypto.getRandomValues(challenge);
|
|
2179
|
+
const userHandle = new TextEncoder().encode(userId);
|
|
2180
|
+
const tempCredentialId = "temp-" + Date.now();
|
|
2181
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "https://localhost";
|
|
2182
|
+
const tempPrfInput = {
|
|
2183
|
+
origin,
|
|
2184
|
+
projectId,
|
|
2185
|
+
credentialId: tempCredentialId
|
|
2186
|
+
};
|
|
2187
|
+
const prfSalt = deriveWrapKey(tempPrfInput);
|
|
2188
|
+
const publicKeyCredentialCreationOptions = {
|
|
2189
|
+
challenge,
|
|
2190
|
+
rp: {
|
|
2191
|
+
name: rpName,
|
|
2192
|
+
id: rpId
|
|
2193
|
+
},
|
|
2194
|
+
user: {
|
|
2195
|
+
id: userHandle,
|
|
2196
|
+
name: userEmail,
|
|
2197
|
+
displayName: userEmail
|
|
2198
|
+
},
|
|
2199
|
+
pubKeyCredParams: PUBKEY_CRED_PARAMS,
|
|
2200
|
+
authenticatorSelection: AUTHENTICATOR_SELECTION,
|
|
2201
|
+
timeout: WEBAUTHN_TIMEOUT,
|
|
2202
|
+
attestation: ATTESTATION,
|
|
2203
|
+
extensions: {
|
|
2204
|
+
prf: {
|
|
2205
|
+
eval: {
|
|
2206
|
+
first: prfSalt.buffer
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
const credential = await navigator.credentials.create({
|
|
2212
|
+
publicKey: publicKeyCredentialCreationOptions
|
|
2213
|
+
});
|
|
2214
|
+
if (!credential || !("response" in credential)) {
|
|
2215
|
+
throw new Error("Failed to create passkey credential");
|
|
2216
|
+
}
|
|
2217
|
+
const credentialId = Array.from(new Uint8Array(credential.rawId)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2218
|
+
const extensionResults = credential.getClientExtensionResults();
|
|
2219
|
+
if (!extensionResults.prf || !extensionResults.prf.results || !extensionResults.prf.results.first) {
|
|
2220
|
+
throw new Error("PRF extension not supported or PRF output missing. Please use a browser that supports WebAuthn PRF extension.");
|
|
2221
|
+
}
|
|
2222
|
+
const prfOutputBuffer = extensionResults.prf.results.first;
|
|
2223
|
+
const prfOutput = new Uint8Array(prfOutputBuffer);
|
|
2224
|
+
const prfInput = {
|
|
2225
|
+
origin,
|
|
2226
|
+
projectId,
|
|
2227
|
+
credentialId
|
|
2228
|
+
};
|
|
2229
|
+
const wrapKey = prfOutput;
|
|
2230
|
+
const masterKeyProvider = createMasterKeyProvider();
|
|
2231
|
+
const masterSeedHandle = await masterKeyProvider.generate();
|
|
2232
|
+
try {
|
|
2233
|
+
const keyStorageType = "passkey";
|
|
2234
|
+
const version = "v1";
|
|
2235
|
+
const aadBytes = new TextEncoder().encode(
|
|
2236
|
+
`volr/master-seed/v1|${userId}|${keyStorageType}|${version}`
|
|
2237
|
+
);
|
|
2238
|
+
const encryptedBlob = await sealMasterSeed(
|
|
2239
|
+
masterSeedHandle.bytes,
|
|
2240
|
+
wrapKey,
|
|
2241
|
+
aadBytes
|
|
2242
|
+
);
|
|
2243
|
+
const blob = new Blob(
|
|
2244
|
+
[
|
|
2245
|
+
encryptedBlob.cipher,
|
|
2246
|
+
encryptedBlob.nonce
|
|
2247
|
+
],
|
|
2248
|
+
{
|
|
2249
|
+
type: "application/octet-stream"
|
|
2250
|
+
}
|
|
2251
|
+
);
|
|
2252
|
+
const accessToken = client.getAccessToken();
|
|
2253
|
+
if (!accessToken) {
|
|
2254
|
+
throw new Error("Access token is required for blob upload");
|
|
2255
|
+
}
|
|
2256
|
+
const { key: blobUrl } = await uploadBlob({
|
|
2257
|
+
baseUrl,
|
|
2258
|
+
apiKey,
|
|
2259
|
+
accessToken,
|
|
2260
|
+
blob
|
|
2261
|
+
// Don't pass axiosInstance - let uploadBlob create its own
|
|
2262
|
+
});
|
|
2263
|
+
if (!blobUrl) {
|
|
2264
|
+
throw new Error("Failed to upload blob: missing key");
|
|
2265
|
+
}
|
|
2266
|
+
const keypair = deriveEvmKey({ masterSeed: masterSeedHandle.bytes });
|
|
2267
|
+
const address = keypair.address;
|
|
2268
|
+
const registerResponse = await client.post("/wallet/provider/register", {
|
|
2269
|
+
keyStorageType: "passkey",
|
|
2270
|
+
credentialId,
|
|
2271
|
+
blobUrl,
|
|
2272
|
+
prfInput: {
|
|
2273
|
+
origin,
|
|
2274
|
+
projectId,
|
|
2275
|
+
credentialId
|
|
2276
|
+
},
|
|
2277
|
+
address
|
|
2278
|
+
});
|
|
2279
|
+
return {
|
|
2280
|
+
credentialId,
|
|
2281
|
+
blobUrl,
|
|
2282
|
+
prfInput,
|
|
2283
|
+
address,
|
|
2284
|
+
encryptedBlob: {
|
|
2285
|
+
cipher: encryptedBlob.cipher,
|
|
2286
|
+
nonce: encryptedBlob.nonce
|
|
2287
|
+
},
|
|
2288
|
+
aad: aadBytes,
|
|
2289
|
+
user: registerResponse.user
|
|
2290
|
+
};
|
|
2291
|
+
} finally {
|
|
2292
|
+
masterSeedHandle.destroy();
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
function usePasskeyEnrollment() {
|
|
2296
|
+
const { config, setProvider, setUser, user } = useVolr();
|
|
2297
|
+
const { client } = useInternalAuth();
|
|
2298
|
+
const [step, setStep] = useState("idle");
|
|
2299
|
+
const [isEnrolling, setIsEnrolling] = useState(false);
|
|
2300
|
+
const [error, setError] = useState(null);
|
|
2301
|
+
const isEnrollingRef = useRef(false);
|
|
2302
|
+
const enroll = useCallback(async () => {
|
|
2303
|
+
if (isEnrollingRef.current || isEnrolling) return;
|
|
2304
|
+
isEnrollingRef.current = true;
|
|
2305
|
+
setIsEnrolling(true);
|
|
2306
|
+
setError(null);
|
|
2307
|
+
setStep("creating");
|
|
2308
|
+
try {
|
|
2309
|
+
if (!user?.id) {
|
|
2310
|
+
throw new Error("User ID is required for passkey enrollment");
|
|
2311
|
+
}
|
|
2312
|
+
if (!user?.email) {
|
|
2313
|
+
throw new Error("User email is required for passkey enrollment");
|
|
2314
|
+
}
|
|
2315
|
+
const accessToken = client.getAccessToken();
|
|
2316
|
+
if (!accessToken) {
|
|
2317
|
+
throw new Error("Access token is required for passkey enrollment. Please login first.");
|
|
2318
|
+
}
|
|
2319
|
+
const projectId = user.id;
|
|
2320
|
+
setStep("encrypting");
|
|
2321
|
+
const result = await enrollPasskey({
|
|
2322
|
+
client,
|
|
2323
|
+
baseUrl: config.apiBaseUrl,
|
|
2324
|
+
apiKey: config.projectApiKey,
|
|
2325
|
+
userId: user.id,
|
|
2326
|
+
userEmail: user.email,
|
|
2327
|
+
projectId
|
|
2328
|
+
});
|
|
2329
|
+
setStep("registering");
|
|
2330
|
+
if (typeof window !== "undefined") {
|
|
2331
|
+
safeStorage.setItem(STORAGE_KEYS.credentialId, result.credentialId);
|
|
2332
|
+
}
|
|
2333
|
+
const passkeyAdapter = createPasskeyAdapter({
|
|
2334
|
+
rpId: typeof window !== "undefined" ? window.location.hostname : "localhost",
|
|
2335
|
+
rpName: "Volr"
|
|
2336
|
+
});
|
|
2337
|
+
const provider = createPasskeyProvider(passkeyAdapter, {
|
|
2338
|
+
prfInput: result.prfInput,
|
|
2339
|
+
encryptedBlob: result.encryptedBlob,
|
|
2340
|
+
aad: result.aad
|
|
2341
|
+
});
|
|
2342
|
+
if (result.user) {
|
|
2343
|
+
setUser(result.user);
|
|
2344
|
+
} else {
|
|
2345
|
+
const nextUser = {
|
|
2346
|
+
...user,
|
|
2347
|
+
id: user.id,
|
|
2348
|
+
email: user.email,
|
|
2349
|
+
keyStorageType: "passkey",
|
|
2350
|
+
evmAddress: result.address,
|
|
2351
|
+
blobUrl: result.blobUrl,
|
|
2352
|
+
prfInput: result.prfInput,
|
|
2353
|
+
credentialId: result.credentialId
|
|
2354
|
+
};
|
|
2355
|
+
setUser(nextUser);
|
|
2356
|
+
}
|
|
2357
|
+
await setProvider(provider);
|
|
2358
|
+
setStep("idle");
|
|
2359
|
+
} catch (err) {
|
|
2360
|
+
const error2 = err instanceof Error ? err : new Error("Failed to enroll passkey");
|
|
2361
|
+
setError(error2);
|
|
2362
|
+
setStep("idle");
|
|
2363
|
+
throw error2;
|
|
2364
|
+
} finally {
|
|
2365
|
+
setIsEnrolling(false);
|
|
2366
|
+
isEnrollingRef.current = false;
|
|
2367
|
+
}
|
|
2368
|
+
}, [
|
|
2369
|
+
client,
|
|
2370
|
+
config.apiBaseUrl,
|
|
2371
|
+
config.projectApiKey,
|
|
2372
|
+
user,
|
|
2373
|
+
setProvider,
|
|
2374
|
+
setUser,
|
|
2375
|
+
isEnrolling
|
|
2376
|
+
]);
|
|
2377
|
+
return {
|
|
2378
|
+
enroll,
|
|
2379
|
+
step,
|
|
2380
|
+
isEnrolling,
|
|
2381
|
+
error
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// src/headless/mpc-transport.ts
|
|
2386
|
+
function createBackendMpcTransport(params) {
|
|
2387
|
+
const { client } = params;
|
|
2388
|
+
let currentSessionToken;
|
|
2389
|
+
let cachedAddress;
|
|
2390
|
+
return {
|
|
2391
|
+
async ensureSession() {
|
|
2392
|
+
if (currentSessionToken) {
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const response = await client.post(
|
|
2396
|
+
"/wallet/mpc/connect",
|
|
2397
|
+
{}
|
|
2398
|
+
);
|
|
2399
|
+
currentSessionToken = response.sessionToken;
|
|
2400
|
+
},
|
|
2401
|
+
async getAddress() {
|
|
2402
|
+
if (cachedAddress) {
|
|
2403
|
+
return cachedAddress;
|
|
2404
|
+
}
|
|
2405
|
+
await this.ensureSession();
|
|
2406
|
+
const response = await client.get(
|
|
2407
|
+
"/wallet/mpc/address"
|
|
2408
|
+
);
|
|
2409
|
+
cachedAddress = response.address;
|
|
2410
|
+
return cachedAddress;
|
|
2411
|
+
},
|
|
2412
|
+
async signMessage(hash32) {
|
|
2413
|
+
await this.ensureSession();
|
|
2414
|
+
const hashHex = Array.from(hash32).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2415
|
+
const response = await client.post("/wallet/mpc/sign", {
|
|
2416
|
+
hash: hashHex
|
|
2417
|
+
});
|
|
2418
|
+
const rHex = response.r.startsWith("0x") ? response.r.slice(2) : response.r;
|
|
2419
|
+
const sHex = response.s.startsWith("0x") ? response.s.slice(2) : response.s;
|
|
2420
|
+
const r = new Uint8Array(32);
|
|
2421
|
+
const s = new Uint8Array(32);
|
|
2422
|
+
for (let i = 0; i < 32; i++) {
|
|
2423
|
+
r[i] = parseInt(rHex.slice(i * 2, i * 2 + 2), 16);
|
|
2424
|
+
s[i] = parseInt(sHex.slice(i * 2, i * 2 + 2), 16);
|
|
2425
|
+
}
|
|
2426
|
+
return {
|
|
2427
|
+
r,
|
|
2428
|
+
s,
|
|
2429
|
+
yParity: response.yParity
|
|
2430
|
+
};
|
|
2431
|
+
},
|
|
2432
|
+
async signTypedData(input) {
|
|
2433
|
+
await this.ensureSession();
|
|
2434
|
+
const response = await client.post(
|
|
2435
|
+
"/wallet/mpc/sign-typed",
|
|
2436
|
+
{
|
|
2437
|
+
domain: input.domain,
|
|
2438
|
+
types: input.types,
|
|
2439
|
+
message: input.message
|
|
2440
|
+
}
|
|
2441
|
+
);
|
|
2442
|
+
return response.signature;
|
|
2443
|
+
},
|
|
2444
|
+
async getPublicKey() {
|
|
2445
|
+
await this.ensureSession();
|
|
2446
|
+
const response = await client.get(
|
|
2447
|
+
"/wallet/mpc/public-key"
|
|
2448
|
+
);
|
|
2449
|
+
const pubKeyHex = response.publicKey.startsWith("0x") ? response.publicKey.slice(2) : response.publicKey;
|
|
2450
|
+
const pubKey = new Uint8Array(65);
|
|
2451
|
+
for (let i = 0; i < 65; i++) {
|
|
2452
|
+
pubKey[i] = parseInt(pubKeyHex.slice(i * 2, i * 2 + 2), 16);
|
|
2453
|
+
}
|
|
2454
|
+
return pubKey;
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// src/headless/wallet-provider.ts
|
|
2460
|
+
async function registerWalletProvider(params) {
|
|
2461
|
+
const { client, keyStorageType, address, credentialId, blobUrl, prfInput } = params;
|
|
2462
|
+
if (!keyStorageType) {
|
|
2463
|
+
throw new Error("keyStorageType is required");
|
|
2464
|
+
}
|
|
2465
|
+
if (!address) {
|
|
2466
|
+
throw new Error("address is required");
|
|
2467
|
+
}
|
|
2468
|
+
if (keyStorageType === "passkey") {
|
|
2469
|
+
if (!credentialId) {
|
|
2470
|
+
throw new Error("credentialId is required for passkey");
|
|
2471
|
+
}
|
|
2472
|
+
if (!blobUrl) {
|
|
2473
|
+
throw new Error("blobUrl is required for passkey");
|
|
2474
|
+
}
|
|
2475
|
+
if (!prfInput) {
|
|
2476
|
+
throw new Error("prfInput is required for passkey");
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
await client.post("/wallet/provider/register", {
|
|
2480
|
+
keyStorageType,
|
|
2481
|
+
address,
|
|
2482
|
+
...keyStorageType === "passkey" && {
|
|
2483
|
+
credentialId,
|
|
2484
|
+
blobUrl,
|
|
2485
|
+
prfInput
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
function useMpcConnection() {
|
|
2490
|
+
const { setProvider } = useVolr();
|
|
2491
|
+
const { client } = useInternalAuth();
|
|
2492
|
+
const [step, setStep] = useState("idle");
|
|
2493
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
2494
|
+
const [error, setError] = useState(null);
|
|
2495
|
+
const connect = useCallback(async () => {
|
|
2496
|
+
if (isConnecting) return;
|
|
2497
|
+
setIsConnecting(true);
|
|
2498
|
+
setError(null);
|
|
2499
|
+
setStep("connecting");
|
|
2500
|
+
try {
|
|
2501
|
+
const transport = createBackendMpcTransport({ client });
|
|
2502
|
+
await transport.ensureSession();
|
|
2503
|
+
const address = await transport.getAddress();
|
|
2504
|
+
setStep("registering");
|
|
2505
|
+
await registerWalletProvider({
|
|
2506
|
+
client,
|
|
2507
|
+
keyStorageType: "mpc",
|
|
2508
|
+
address
|
|
2509
|
+
});
|
|
2510
|
+
const provider = createMpcProvider(transport);
|
|
2511
|
+
await setProvider(provider);
|
|
2512
|
+
setStep("idle");
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
const error2 = err instanceof Error ? err : new Error("Failed to connect to MPC provider");
|
|
2515
|
+
setError(error2);
|
|
2516
|
+
setStep("idle");
|
|
2517
|
+
throw error2;
|
|
2518
|
+
} finally {
|
|
2519
|
+
setIsConnecting(false);
|
|
2520
|
+
}
|
|
2521
|
+
}, [client, setProvider, isConnecting]);
|
|
2522
|
+
return {
|
|
2523
|
+
connect,
|
|
2524
|
+
step,
|
|
2525
|
+
isConnecting,
|
|
2526
|
+
error
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
// src/headless/blobs.ts
|
|
2531
|
+
async function uploadBlobViaPresign(params) {
|
|
2532
|
+
const { baseUrl, apiKey, blob, contentType = "application/octet-stream" } = params;
|
|
2533
|
+
const api = createAxiosInstance(baseUrl, apiKey);
|
|
2534
|
+
const presignResponse = await api.post("/blob/presign", {
|
|
2535
|
+
op: "put",
|
|
2536
|
+
contentType
|
|
2537
|
+
});
|
|
2538
|
+
const presignData = presignResponse.data?.data || presignResponse.data;
|
|
2539
|
+
const uploadUrl = presignData.url;
|
|
2540
|
+
const s3Key = presignData.s3Key;
|
|
2541
|
+
const putRes = await fetch(uploadUrl, {
|
|
2542
|
+
method: "PUT",
|
|
2543
|
+
body: blob,
|
|
2544
|
+
headers: {
|
|
2545
|
+
"Content-Type": contentType,
|
|
2546
|
+
"x-amz-server-side-encryption": "AES256"
|
|
2547
|
+
},
|
|
2548
|
+
mode: "cors"
|
|
2549
|
+
});
|
|
2550
|
+
if (!putRes.ok) {
|
|
2551
|
+
const text = await putRes.text().catch(() => "");
|
|
2552
|
+
throw new Error(`Failed to upload to S3 (${putRes.status}): ${text || putRes.statusText}`);
|
|
2553
|
+
}
|
|
2554
|
+
return { s3Key };
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
export { DEFAULT_EXPIRES_IN_SEC, DEFAULT_MODE, VolrProvider, buildCall, buildCalls, createGetNetworkInfo, createPasskeyAdapter, defaultIdempotencyKey, normalizeHex, normalizeHexArray, uploadBlobViaPresign, useDepositListener, useInternalAuth, useMpcConnection, usePasskeyEnrollment, usePrecheck, useRelay, useVolr, useVolrLogin, useVolrWallet };
|
|
2558
|
+
//# sourceMappingURL=index.js.map
|
|
2559
|
+
//# sourceMappingURL=index.js.map
|