@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/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