@hfunlabs/hypurr-connect 0.1.1 → 0.1.3

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 CHANGED
@@ -1,20 +1,21 @@
1
1
  // src/HypurrConnectProvider.tsx
2
2
  import {
3
3
  ExchangeClient,
4
- HttpTransport,
5
- InfoClient
4
+ HttpTransport
6
5
  } from "@hfunlabs/hyperliquid";
7
- import { PrivateKeySigner } from "@hfunlabs/hyperliquid/signing";
6
+ import { PrivateKeySigner, signUserSignedAction } from "@hfunlabs/hyperliquid/signing";
8
7
  import {
9
8
  createContext,
10
9
  useCallback,
11
10
  useContext,
12
11
  useEffect,
13
12
  useMemo,
13
+ useRef,
14
14
  useState
15
15
  } from "react";
16
16
 
17
17
  // src/agent.ts
18
+ var AGENT_NAME = "hypurr-connect";
18
19
  var AGENT_STORAGE_PREFIX = "hypurr-connect-agent";
19
20
  function storageKey(masterAddress) {
20
21
  return `${AGENT_STORAGE_PREFIX}:${masterAddress.toLowerCase()}`;
@@ -41,6 +42,39 @@ async function generateAgentKey() {
41
42
  const signer = new PrivateKeySigner2(privateKey);
42
43
  return { privateKey, address: signer.address };
43
44
  }
45
+ async function fetchActiveAgent(userAddress, isTestnet) {
46
+ const url = isTestnet ? "https://api.hyperliquid-testnet.xyz/info" : "https://api.hyperliquid.xyz/info";
47
+ const res = await fetch(url, {
48
+ method: "POST",
49
+ headers: { "Content-Type": "application/json" },
50
+ body: JSON.stringify({ type: "extraAgents", user: userAddress })
51
+ });
52
+ if (!res.ok) return null;
53
+ const agents = await res.json();
54
+ if (!Array.isArray(agents)) return null;
55
+ const nowMs = Date.now();
56
+ const match = agents.find(
57
+ (a) => a.name === AGENT_NAME && a.validUntil * 1e3 > nowMs
58
+ );
59
+ if (!match) return null;
60
+ return { ...match, validUntil: match.validUntil * 1e3 };
61
+ }
62
+ async function isAgentValid(stored, userAddress, isTestnet) {
63
+ if (stored.validUntil <= Date.now()) return false;
64
+ const remote = await fetchActiveAgent(userAddress, isTestnet);
65
+ if (!remote) return false;
66
+ return remote.address.toLowerCase() === stored.address.toLowerCase() && remote.validUntil > Date.now();
67
+ }
68
+ var DEAD_AGENT_PATTERNS = [
69
+ /agent address .+ is not valid/i,
70
+ /unknown signer/i,
71
+ /not authorized/i,
72
+ /not an agent/i
73
+ ];
74
+ function isDeadAgentError(err) {
75
+ const msg = err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : String(err);
76
+ return DEAD_AGENT_PATTERNS.some((p) => p.test(msg));
77
+ }
44
78
 
45
79
  // src/grpc.ts
46
80
  import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
@@ -152,6 +186,14 @@ function useHypurrConnect() {
152
186
  );
153
187
  return ctx;
154
188
  }
189
+ function useHypurrConnectInternal() {
190
+ const ctx = useContext(HypurrConnectContext);
191
+ if (!ctx)
192
+ throw new Error(
193
+ "useHypurrConnectInternal must be used within <HypurrConnectProvider>"
194
+ );
195
+ return ctx;
196
+ }
155
197
  function HypurrConnectProvider({
156
198
  config,
157
199
  children
@@ -175,6 +217,7 @@ function HypurrConnectProvider({
175
217
  () => tgLoginData ? toAuthDataMap(tgLoginData) : {},
176
218
  [tgLoginData]
177
219
  );
220
+ const [tgUserTick, setTgUserTick] = useState(0);
178
221
  useEffect(() => {
179
222
  if (!tgLoginData) return;
180
223
  let cancelled = false;
@@ -183,9 +226,7 @@ function HypurrConnectProvider({
183
226
  (async () => {
184
227
  try {
185
228
  const authData = toAuthDataMap(tgLoginData);
186
- console.log(authData);
187
229
  const { response } = await tgClient.telegramUser({ authData });
188
- console.log(response);
189
230
  if (cancelled) return;
190
231
  setTgUser(response.user ?? null);
191
232
  } catch (err) {
@@ -199,27 +240,56 @@ function HypurrConnectProvider({
199
240
  return () => {
200
241
  cancelled = true;
201
242
  };
202
- }, [tgLoginData, tgClient]);
243
+ }, [tgLoginData, tgClient, tgUserTick]);
203
244
  const [eoaAddress, setEoaAddress] = useState(null);
204
245
  const [agent, setAgent] = useState(null);
246
+ const [eoaLoading, setEoaLoading] = useState(false);
247
+ const [eoaError, setEoaError] = useState(null);
248
+ const eoaSignerRef = useRef(null);
249
+ const authMethod = tgLoginData ? "telegram" : eoaAddress ? "eoa" : null;
250
+ const [wallets, setWallets] = useState([]);
251
+ const [selectedWalletId, setSelectedWalletId] = useState(0);
252
+ const [packs, setPacks] = useState([]);
253
+ const refreshWallets = useCallback(() => setTgUserTick((t) => t + 1), []);
205
254
  useEffect(() => {
206
- if (eoaAddress) {
207
- setAgent(loadAgent(eoaAddress));
208
- } else {
209
- setAgent(null);
255
+ if (authMethod !== "telegram" || !tgUser) {
256
+ setWallets([]);
257
+ setSelectedWalletId(0);
258
+ setPacks([]);
259
+ return;
210
260
  }
211
- }, [eoaAddress]);
212
- const authMethod = tgLoginData ? "telegram" : eoaAddress ? "eoa" : null;
213
- const tgWallet = tgUser?.wallet ?? (tgUser?.wallets ?? [])[0] ?? null;
261
+ const userWallets = tgUser.wallets ?? [];
262
+ setWallets(userWallets);
263
+ setPacks(tgUser.packs ?? []);
264
+ const defaultId = tgUser.walletId || userWallets[0]?.id || 0;
265
+ setSelectedWalletId((prev) => {
266
+ if (prev && userWallets.some((w) => w.id === prev)) return prev;
267
+ return defaultId;
268
+ });
269
+ }, [authMethod, tgUser]);
270
+ const selectedWallet = useMemo(
271
+ () => wallets.find((w) => w.id === selectedWalletId) ?? wallets[0] ?? null,
272
+ [wallets, selectedWalletId]
273
+ );
274
+ const selectWallet = useCallback(
275
+ (walletId) => {
276
+ if (wallets.some((w) => w.id === walletId)) {
277
+ setSelectedWalletId(walletId);
278
+ }
279
+ },
280
+ [wallets]
281
+ );
214
282
  const user = useMemo(() => {
215
- if (tgLoginData && authMethod === "telegram") {
283
+ if (tgLoginData && authMethod === "telegram" && selectedWallet) {
216
284
  return {
217
- address: tgWallet?.ethereumAddress ?? "",
218
- walletId: tgUser?.walletId ?? tgWallet?.id ?? 0,
285
+ address: selectedWallet.ethereumAddress,
286
+ walletId: selectedWallet.id,
219
287
  displayName: tgLoginData.username ? `@${tgLoginData.username}` : tgLoginData.first_name,
220
288
  photoUrl: tgLoginData.photo_url,
221
289
  authMethod: "telegram",
222
- telegramId: String(tgLoginData.id)
290
+ telegramId: String(tgLoginData.id),
291
+ hfunScore: tgUser?.reputation?.hfunScore,
292
+ reputationScore: tgUser?.reputation?.reputationScore
223
293
  };
224
294
  }
225
295
  if (eoaAddress && authMethod === "eoa") {
@@ -231,7 +301,23 @@ function HypurrConnectProvider({
231
301
  };
232
302
  }
233
303
  return null;
234
- }, [tgLoginData, tgUser, tgWallet, eoaAddress, authMethod]);
304
+ }, [tgLoginData, selectedWallet, eoaAddress, authMethod, tgUser]);
305
+ const onDeadAgentRef = useRef(
306
+ null
307
+ );
308
+ onDeadAgentRef.current = (addr) => {
309
+ clearAgent(addr);
310
+ setAgent(null);
311
+ setEoaError("Agent expired or was deregistered. Please reconnect.");
312
+ };
313
+ const agentSignerRef = useRef(
314
+ agent ? new PrivateKeySigner(agent.privateKey) : null
315
+ );
316
+ useEffect(() => {
317
+ agentSignerRef.current = agent ? new PrivateKeySigner(agent.privateKey) : null;
318
+ }, [agent]);
319
+ const provisioningRef = useRef(null);
320
+ const agentReady = authMethod === "telegram" || authMethod === "eoa" && !!agent;
235
321
  const exchange = useMemo(() => {
236
322
  if (authMethod === "telegram" && user?.address) {
237
323
  const transport = new GrpcExchangeTransport({
@@ -246,127 +332,229 @@ function HypurrConnectProvider({
246
332
  userAddress: user.address
247
333
  });
248
334
  }
249
- if (authMethod === "eoa" && agent) {
250
- const wallet = new PrivateKeySigner(agent.privateKey);
251
- return new ExchangeClient({
252
- transport: new HttpTransport({
253
- isTestnet: config.isTestnet ?? false
254
- }),
255
- wallet
256
- });
257
- }
258
- return null;
259
- }, [authMethod, user, agent, config.isTestnet, tgClient, authDataMap]);
260
- const infoClient = useMemo(
261
- () => new InfoClient({
262
- transport: new HttpTransport({
263
- isTestnet: config.isTestnet ?? false
264
- })
265
- }),
266
- [config.isTestnet]
267
- );
268
- const [usdcBalance, setUsdcBalance] = useState(null);
269
- const [usdcBalanceLoading, setUsdcBalanceLoading] = useState(false);
270
- const [balanceTick, setBalanceTick] = useState(0);
271
- const refreshBalance = useCallback(() => setBalanceTick((t) => t + 1), []);
272
- useEffect(() => {
273
- const addr = user?.address;
274
- if (!addr) {
275
- setUsdcBalance(null);
276
- return;
277
- }
278
- let cancelled = false;
279
- setUsdcBalanceLoading(true);
280
- (async () => {
281
- try {
282
- const state = await infoClient.clearinghouseState({
283
- user: addr
335
+ if (authMethod === "eoa" && eoaAddress) {
336
+ const hasSigner = !!eoaSignerRef.current;
337
+ if (!agent && !hasSigner) {
338
+ const noAgentTransport = {
339
+ isTestnet: config.isTestnet ?? false,
340
+ request() {
341
+ throw new Error(
342
+ "[HypurrConnect] No agent key approved and no wallet signer available. Either call approveAgent(signTypedDataAsync) or pass a signer to connectEoa(address, { signTypedData, chainId })."
343
+ );
344
+ }
345
+ };
346
+ return new ExchangeClient({
347
+ transport: noAgentTransport,
348
+ externalSigning: true,
349
+ userAddress: eoaAddress
284
350
  });
285
- if (!cancelled) {
286
- setUsdcBalance(state.withdrawable);
287
- }
288
- } catch (err) {
289
- console.error("[HypurrConnect] Failed to fetch USDC balance:", err);
290
- if (!cancelled) setUsdcBalance(null);
291
- } finally {
292
- if (!cancelled) setUsdcBalanceLoading(false);
293
351
  }
294
- })();
295
- return () => {
296
- cancelled = true;
297
- };
298
- }, [user?.address, infoClient, balanceTick]);
299
- const approveAgent = useCallback(
300
- async (signTypedDataAsync) => {
301
- if (!eoaAddress) throw new Error("No EOA address connected");
302
- const { privateKey, address: agentAddress } = await generateAgentKey();
303
352
  const isTestnet = config.isTestnet ?? false;
304
- const nonce = Date.now();
305
- const action = {
306
- type: "approveAgent",
307
- signatureChainId: isTestnet ? "0x66eee" : "0xa4b1",
308
- hyperliquidChain: isTestnet ? "Testnet" : "Mainnet",
309
- agentAddress: agentAddress.toLowerCase(),
310
- agentName: null,
311
- nonce
353
+ const inner = new HttpTransport({ isTestnet });
354
+ const deadAgentAddr = eoaAddress;
355
+ const guardedTransport = {
356
+ isTestnet: inner.isTestnet,
357
+ async request(endpoint, payload, signal) {
358
+ try {
359
+ return await inner.request(endpoint, payload, signal);
360
+ } catch (err) {
361
+ if (endpoint === "exchange" && isDeadAgentError(err)) {
362
+ onDeadAgentRef.current?.(deadAgentAddr);
363
+ }
364
+ throw err;
365
+ }
366
+ }
312
367
  };
313
- const types = {
314
- "HyperliquidTransaction:ApproveAgent": [
315
- { name: "hyperliquidChain", type: "string" },
316
- { name: "agentAddress", type: "address" },
317
- { name: "agentName", type: "string" },
318
- { name: "nonce", type: "uint64" }
319
- ]
368
+ const signerRef = eoaSignerRef;
369
+ const agentRef = agentSignerRef;
370
+ const provRef = provisioningRef;
371
+ const ownerAddress = eoaAddress;
372
+ const ensureAgent = async () => {
373
+ const existing = agentRef.current;
374
+ if (existing) return existing;
375
+ if (provRef.current) return provRef.current;
376
+ const signer = signerRef.current;
377
+ if (!signer) {
378
+ throw new Error(
379
+ "[HypurrConnect] No wallet signer available to auto-provision agent. Pass a signer to connectEoa(address, { signTypedData, chainId })."
380
+ );
381
+ }
382
+ provRef.current = (async () => {
383
+ try {
384
+ const { privateKey, address: agentAddress } = await generateAgentKey();
385
+ const chainIdHex = `0x${signer.chainId.toString(16)}`;
386
+ const nonce = Date.now();
387
+ const action = {
388
+ type: "approveAgent",
389
+ signatureChainId: chainIdHex,
390
+ hyperliquidChain: isTestnet ? "Testnet" : "Mainnet",
391
+ agentAddress: agentAddress.toLowerCase(),
392
+ agentName: AGENT_NAME,
393
+ nonce
394
+ };
395
+ const approveAgentTypes = {
396
+ "HyperliquidTransaction:ApproveAgent": [
397
+ { name: "hyperliquidChain", type: "string" },
398
+ { name: "agentAddress", type: "address" },
399
+ { name: "agentName", type: "string" },
400
+ { name: "nonce", type: "uint64" }
401
+ ]
402
+ };
403
+ const wallet = {
404
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
405
+ signTypedData(params) {
406
+ return signer.signTypedData(params);
407
+ },
408
+ getAddresses: async () => [ownerAddress],
409
+ getChainId: async () => signer.chainId
410
+ };
411
+ const signature = await signUserSignedAction({
412
+ wallet,
413
+ action,
414
+ types: approveAgentTypes
415
+ });
416
+ const apiUrl = isTestnet ? "https://api.hyperliquid-testnet.xyz/exchange" : "https://api.hyperliquid.xyz/exchange";
417
+ const res = await fetch(apiUrl, {
418
+ method: "POST",
419
+ headers: { "Content-Type": "application/json" },
420
+ body: JSON.stringify({ action, signature, nonce })
421
+ });
422
+ const body = await res.json();
423
+ if (body?.status === "err") {
424
+ throw new Error(
425
+ `approveAgent API error: ${body.response ?? JSON.stringify(body)}`
426
+ );
427
+ }
428
+ const remote = await fetchActiveAgent(ownerAddress, isTestnet);
429
+ const validUntil = remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1e3;
430
+ const stored = {
431
+ privateKey,
432
+ address: agentAddress,
433
+ approvedAt: Date.now(),
434
+ validUntil
435
+ };
436
+ saveAgent(ownerAddress, stored);
437
+ const newSigner = new PrivateKeySigner(privateKey);
438
+ agentRef.current = newSigner;
439
+ setAgent(stored);
440
+ return newSigner;
441
+ } finally {
442
+ provRef.current = null;
443
+ }
444
+ })();
445
+ return provRef.current;
320
446
  };
321
- const signature = await signTypedDataAsync({
322
- domain: {
323
- name: "HyperliquidSignTransaction",
324
- version: "1",
325
- chainId: isTestnet ? 421614 : 42161,
326
- verifyingContract: "0x0000000000000000000000000000000000000000"
327
- },
328
- types,
329
- primaryType: "HyperliquidTransaction:ApproveAgent",
330
- message: {
331
- hyperliquidChain: action.hyperliquidChain,
332
- agentAddress: action.agentAddress,
333
- agentName: "",
334
- nonce: BigInt(nonce)
447
+ const dualWallet = {
448
+ address: ownerAddress,
449
+ async signTypedData(params) {
450
+ if (params.domain.name === "HyperliquidSignTransaction") {
451
+ const signer = signerRef.current;
452
+ if (!signer) {
453
+ throw new Error(
454
+ "[HypurrConnect] No wallet signer available for user-signed actions. Pass a signer to connectEoa(address, { signTypedData, chainId })."
455
+ );
456
+ }
457
+ return signer.signTypedData(
458
+ params
459
+ );
460
+ }
461
+ const agentSigner = await ensureAgent();
462
+ return agentSigner.signTypedData(params);
335
463
  }
336
- });
337
- const r = `0x${signature.slice(2, 66)}`;
338
- const s = `0x${signature.slice(66, 130)}`;
339
- const v = parseInt(signature.slice(130, 132), 16);
340
- const url = isTestnet ? "https://api.hyperliquid-testnet.xyz/exchange" : "https://api.hyperliquid.xyz/exchange";
341
- const res = await fetch(url, {
342
- method: "POST",
343
- headers: { "Content-Type": "application/json" },
344
- body: JSON.stringify({
345
- action,
346
- nonce,
347
- signature: { r, s, v }
348
- })
349
- });
350
- const body = await res.json();
351
- if (body?.status !== "ok") {
352
- throw new Error(`approveAgent failed: ${JSON.stringify(body)}`);
353
- }
354
- const stored = {
355
- privateKey,
356
- address: agentAddress,
357
- approvedAt: Date.now()
358
464
  };
359
- saveAgent(eoaAddress, stored);
360
- setAgent(stored);
361
- },
362
- [eoaAddress, config.isTestnet]
363
- );
465
+ return new ExchangeClient({
466
+ transport: guardedTransport,
467
+ wallet: dualWallet,
468
+ signatureChainId: () => {
469
+ const id = signerRef.current?.chainId ?? 42161;
470
+ return `0x${id.toString(16)}`;
471
+ }
472
+ });
473
+ }
474
+ return null;
475
+ }, [
476
+ authMethod,
477
+ user,
478
+ agent,
479
+ eoaAddress,
480
+ config.isTestnet,
481
+ tgClient,
482
+ authDataMap
483
+ ]);
364
484
  const handleClearAgent = useCallback(() => {
365
485
  if (eoaAddress) {
366
486
  clearAgent(eoaAddress);
367
487
  setAgent(null);
368
488
  }
369
489
  }, [eoaAddress]);
490
+ const createWallet = useCallback(
491
+ async (name) => {
492
+ const { response } = await tgClient.hyperliquidWalletCreate({
493
+ authData: authDataMap,
494
+ name
495
+ });
496
+ refreshWallets();
497
+ if (!response.wallet)
498
+ throw new Error("Wallet creation returned no wallet");
499
+ return response.wallet;
500
+ },
501
+ [tgClient, authDataMap, refreshWallets]
502
+ );
503
+ const deleteWallet = useCallback(
504
+ async (walletId) => {
505
+ await tgClient.hyperliquidWalletDelete({
506
+ authData: authDataMap,
507
+ walletId
508
+ });
509
+ if (walletId === selectedWalletId) {
510
+ const remaining = wallets.filter((w) => w.id !== walletId);
511
+ setSelectedWalletId(remaining[0]?.id ?? 0);
512
+ }
513
+ refreshWallets();
514
+ },
515
+ [tgClient, authDataMap, selectedWalletId, wallets, refreshWallets]
516
+ );
517
+ const createWalletPack = useCallback(
518
+ async (name) => {
519
+ const { response } = await tgClient.telegramChatWalletPackCreate({
520
+ authData: authDataMap,
521
+ name
522
+ });
523
+ refreshWallets();
524
+ return response.packId;
525
+ },
526
+ [tgClient, authDataMap, refreshWallets]
527
+ );
528
+ const addPackLabel = useCallback(
529
+ async (params) => {
530
+ await tgClient.telegramChatWalletPackLabelAdd({
531
+ authData: authDataMap,
532
+ ...params
533
+ });
534
+ refreshWallets();
535
+ },
536
+ [tgClient, authDataMap, refreshWallets]
537
+ );
538
+ const modifyPackLabel = useCallback(
539
+ async (params) => {
540
+ await tgClient.telegramChatWalletPackLabelModify({
541
+ authData: authDataMap,
542
+ ...params
543
+ });
544
+ refreshWallets();
545
+ },
546
+ [tgClient, authDataMap, refreshWallets]
547
+ );
548
+ const removePackLabel = useCallback(
549
+ async (params) => {
550
+ await tgClient.telegramChatWalletPackLabelRemove({
551
+ authData: authDataMap,
552
+ ...params
553
+ });
554
+ refreshWallets();
555
+ },
556
+ [tgClient, authDataMap, refreshWallets]
557
+ );
370
558
  const [loginModalOpen, setLoginModalOpen] = useState(false);
371
559
  const openLoginModal = useCallback(() => setLoginModalOpen(true), []);
372
560
  const closeLoginModal = useCallback(() => setLoginModalOpen(false), []);
@@ -375,44 +563,155 @@ function HypurrConnectProvider({
375
563
  localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(data));
376
564
  setEoaAddress(null);
377
565
  setAgent(null);
566
+ setEoaError(null);
378
567
  }, []);
379
- const loginEoa = useCallback((address) => {
380
- setEoaAddress(address);
381
- setTgLoginData(null);
382
- setTgUser(null);
383
- setTgError(null);
384
- localStorage.removeItem(TELEGRAM_STORAGE_KEY);
385
- }, []);
568
+ const connectEoa = useCallback(
569
+ (address, signer) => {
570
+ eoaSignerRef.current = signer ?? null;
571
+ setEoaAddress(address);
572
+ setTgLoginData(null);
573
+ setTgUser(null);
574
+ setTgError(null);
575
+ setEoaError(null);
576
+ localStorage.removeItem(TELEGRAM_STORAGE_KEY);
577
+ const existing = loadAgent(address);
578
+ if (existing && existing.validUntil > Date.now()) {
579
+ setAgent(existing);
580
+ } else {
581
+ if (existing) clearAgent(address);
582
+ setAgent(null);
583
+ }
584
+ },
585
+ []
586
+ );
587
+ const approveAgentFn = useCallback(
588
+ async (signTypedDataAsync, chainId) => {
589
+ if (!eoaAddress) {
590
+ throw new Error(
591
+ "[HypurrConnect] Cannot approve agent: no EOA wallet connected. Call connectEoa(address) first."
592
+ );
593
+ }
594
+ eoaSignerRef.current = { signTypedData: signTypedDataAsync, chainId };
595
+ setEoaLoading(true);
596
+ setEoaError(null);
597
+ try {
598
+ const existing = loadAgent(eoaAddress);
599
+ if (existing) {
600
+ const isTestnet2 = config.isTestnet ?? false;
601
+ const valid = await isAgentValid(existing, eoaAddress, isTestnet2);
602
+ if (valid) {
603
+ setAgent(existing);
604
+ return;
605
+ }
606
+ clearAgent(eoaAddress);
607
+ }
608
+ const { privateKey, address: agentAddress } = await generateAgentKey();
609
+ const isTestnet = config.isTestnet ?? false;
610
+ const chainIdHex = `0x${chainId.toString(16)}`;
611
+ const nonce = Date.now();
612
+ const action = {
613
+ type: "approveAgent",
614
+ signatureChainId: chainIdHex,
615
+ hyperliquidChain: isTestnet ? "Testnet" : "Mainnet",
616
+ agentAddress: agentAddress.toLowerCase(),
617
+ agentName: AGENT_NAME,
618
+ nonce
619
+ };
620
+ const approveAgentTypes = {
621
+ "HyperliquidTransaction:ApproveAgent": [
622
+ { name: "hyperliquidChain", type: "string" },
623
+ { name: "agentAddress", type: "address" },
624
+ { name: "agentName", type: "string" },
625
+ { name: "nonce", type: "uint64" }
626
+ ]
627
+ };
628
+ const wallet = {
629
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
630
+ signTypedData(params) {
631
+ return signTypedDataAsync(params);
632
+ },
633
+ getAddresses: async () => [eoaAddress],
634
+ getChainId: async () => chainId
635
+ };
636
+ const signature = await signUserSignedAction({
637
+ wallet,
638
+ action,
639
+ types: approveAgentTypes
640
+ });
641
+ const apiUrl = isTestnet ? "https://api.hyperliquid-testnet.xyz/exchange" : "https://api.hyperliquid.xyz/exchange";
642
+ const res = await fetch(apiUrl, {
643
+ method: "POST",
644
+ headers: { "Content-Type": "application/json" },
645
+ body: JSON.stringify({ action, signature, nonce })
646
+ });
647
+ const body = await res.json();
648
+ if (body?.status === "err") {
649
+ throw new Error(
650
+ `approveAgent API error: ${body.response ?? JSON.stringify(body)}`
651
+ );
652
+ }
653
+ const remote = await fetchActiveAgent(eoaAddress, isTestnet);
654
+ const validUntil = remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1e3;
655
+ const stored = {
656
+ privateKey,
657
+ address: agentAddress,
658
+ approvedAt: Date.now(),
659
+ validUntil
660
+ };
661
+ saveAgent(eoaAddress, stored);
662
+ setAgent(stored);
663
+ } catch (err) {
664
+ console.error("[HypurrConnect] EOA agent approval failed:", err);
665
+ setEoaError(err instanceof Error ? err.message : String(err));
666
+ setAgent(null);
667
+ } finally {
668
+ setEoaLoading(false);
669
+ }
670
+ },
671
+ [eoaAddress, config.isTestnet]
672
+ );
386
673
  const logout = useCallback(() => {
387
674
  setTgLoginData(null);
388
675
  setTgUser(null);
389
676
  setTgError(null);
390
677
  setEoaAddress(null);
391
678
  setAgent(null);
679
+ setEoaError(null);
680
+ eoaSignerRef.current = null;
392
681
  localStorage.removeItem(TELEGRAM_STORAGE_KEY);
393
682
  }, []);
394
683
  const value = useMemo(
395
684
  () => ({
396
685
  user,
397
686
  isLoggedIn: !!user,
398
- isLoading: tgLoading,
399
- error: tgError,
687
+ isLoading: tgLoading || eoaLoading,
688
+ error: tgError ?? eoaError,
400
689
  authMethod,
401
690
  exchange,
402
- usdcBalance,
403
- usdcBalanceLoading,
404
- refreshBalance,
691
+ wallets,
692
+ selectedWalletId,
693
+ selectWallet,
694
+ createWallet,
695
+ deleteWallet,
696
+ refreshWallets,
697
+ packs,
698
+ createWalletPack,
699
+ addPackLabel,
700
+ modifyPackLabel,
701
+ removePackLabel,
405
702
  loginModalOpen,
406
703
  openLoginModal,
407
704
  closeLoginModal,
408
705
  loginTelegram,
409
- loginEoa,
706
+ connectEoa,
707
+ approveAgent: approveAgentFn,
410
708
  logout,
411
709
  agent,
412
- agentReady: authMethod === "telegram" || !!agent,
413
- approveAgent,
710
+ agentReady,
414
711
  clearAgent: handleClearAgent,
415
712
  botId: config.telegram?.botId ?? "",
713
+ botUsername: config.telegram?.botUsername ?? "",
714
+ useWidget: config.telegram?.useWidget ?? false,
416
715
  authDataMap,
417
716
  telegramClient: tgClient,
418
717
  staticClient
@@ -420,22 +719,35 @@ function HypurrConnectProvider({
420
719
  [
421
720
  user,
422
721
  tgLoading,
722
+ eoaLoading,
423
723
  tgError,
724
+ eoaError,
424
725
  authMethod,
425
726
  exchange,
426
- usdcBalance,
427
- usdcBalanceLoading,
428
- refreshBalance,
727
+ wallets,
728
+ selectedWalletId,
729
+ selectWallet,
730
+ createWallet,
731
+ deleteWallet,
732
+ refreshWallets,
733
+ packs,
734
+ createWalletPack,
735
+ addPackLabel,
736
+ modifyPackLabel,
737
+ removePackLabel,
429
738
  loginModalOpen,
430
739
  openLoginModal,
431
740
  closeLoginModal,
432
741
  loginTelegram,
433
- loginEoa,
742
+ connectEoa,
743
+ approveAgentFn,
434
744
  logout,
435
745
  agent,
436
- approveAgent,
746
+ agentReady,
437
747
  handleClearAgent,
438
748
  config.telegram?.botId,
749
+ config.telegram?.botUsername,
750
+ config.telegram?.useWidget,
439
751
  authDataMap,
440
752
  tgClient,
441
753
  staticClient
@@ -452,7 +764,7 @@ import {
452
764
  } from "framer-motion";
453
765
  import {
454
766
  useCallback as useCallback2,
455
- useEffect as useEffect2,
767
+ useEffect as useEffect3,
456
768
  useSyncExternalStore
457
769
  } from "react";
458
770
 
@@ -583,8 +895,53 @@ function TelegramColorIcon({ style }) {
583
895
  );
584
896
  }
585
897
 
898
+ // src/TelegramLoginWidget.tsx
899
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
900
+ import { jsx as jsx4 } from "react/jsx-runtime";
901
+ var WIDGET_SCRIPT_URL = "https://telegram.org/js/telegram-widget.js?22";
902
+ var CALLBACK_NAME = "__hypurrConnectTelegramAuth";
903
+ function TelegramLoginWidget({
904
+ botUsername,
905
+ onAuth,
906
+ buttonSize = "large",
907
+ cornerRadius,
908
+ showUserPhoto = true,
909
+ requestAccess = true
910
+ }) {
911
+ const containerRef = useRef2(null);
912
+ const onAuthRef = useRef2(onAuth);
913
+ onAuthRef.current = onAuth;
914
+ useEffect2(() => {
915
+ const container = containerRef.current;
916
+ if (!container) return;
917
+ window[CALLBACK_NAME] = (user) => {
918
+ onAuthRef.current(user);
919
+ };
920
+ const script = document.createElement("script");
921
+ script.src = WIDGET_SCRIPT_URL;
922
+ script.async = true;
923
+ script.setAttribute("data-telegram-login", botUsername);
924
+ script.setAttribute("data-size", buttonSize);
925
+ script.setAttribute("data-onauth", `${CALLBACK_NAME}(user)`);
926
+ script.setAttribute("data-userpic", String(showUserPhoto));
927
+ if (requestAccess) {
928
+ script.setAttribute("data-request-access", "write");
929
+ }
930
+ if (cornerRadius !== void 0) {
931
+ script.setAttribute("data-radius", String(cornerRadius));
932
+ }
933
+ container.innerHTML = "";
934
+ container.appendChild(script);
935
+ return () => {
936
+ container.innerHTML = "";
937
+ delete window[CALLBACK_NAME];
938
+ };
939
+ }, [botUsername, buttonSize, cornerRadius, showUserPhoto, requestAccess]);
940
+ return /* @__PURE__ */ jsx4("div", { ref: containerRef });
941
+ }
942
+
586
943
  // src/LoginModal.tsx
587
- import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
944
+ import { Fragment, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
588
945
  var MOBILE_BREAKPOINT = 640;
589
946
  var btnStyle = {
590
947
  display: "flex",
@@ -662,7 +1019,7 @@ function HoverButton({
662
1019
  onClick,
663
1020
  children
664
1021
  }) {
665
- return /* @__PURE__ */ jsx4(
1022
+ return /* @__PURE__ */ jsx5(
666
1023
  motion.button,
667
1024
  {
668
1025
  type: "button",
@@ -674,7 +1031,14 @@ function HoverButton({
674
1031
  );
675
1032
  }
676
1033
  function LoginModal({ onConnectWallet, walletIcon }) {
677
- const { loginTelegram, loginModalOpen, closeLoginModal, botId } = useHypurrConnect();
1034
+ const {
1035
+ loginTelegram,
1036
+ loginModalOpen,
1037
+ closeLoginModal,
1038
+ botId,
1039
+ botUsername,
1040
+ useWidget
1041
+ } = useHypurrConnectInternal();
678
1042
  const handleTelegramAuth = useCallback2(
679
1043
  (user) => {
680
1044
  loginTelegram(user);
@@ -682,7 +1046,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
682
1046
  },
683
1047
  [loginTelegram, closeLoginModal]
684
1048
  );
685
- useEffect2(() => {
1049
+ useEffect3(() => {
686
1050
  if (!loginModalOpen) return;
687
1051
  function onMessage(e) {
688
1052
  if (e.origin !== "https://oauth.telegram.org") return;
@@ -721,7 +1085,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
721
1085
  }, [botId]);
722
1086
  const isMobile = useIsMobile();
723
1087
  const modalContent = /* @__PURE__ */ jsxs3(Fragment, { children: [
724
- /* @__PURE__ */ jsx4(
1088
+ /* @__PURE__ */ jsx5(
725
1089
  "div",
726
1090
  {
727
1091
  style: {
@@ -732,13 +1096,19 @@ function LoginModal({ onConnectWallet, walletIcon }) {
732
1096
  gap: 8,
733
1097
  overflow: "hidden"
734
1098
  },
735
- children: /* @__PURE__ */ jsxs3(HoverButton, { onClick: openTelegramOAuth, children: [
736
- /* @__PURE__ */ jsx4(TelegramColorIcon, { style: iconSize }),
1099
+ children: useWidget && botUsername ? /* @__PURE__ */ jsx5(
1100
+ TelegramLoginWidget,
1101
+ {
1102
+ botUsername,
1103
+ onAuth: handleTelegramAuth
1104
+ }
1105
+ ) : /* @__PURE__ */ jsxs3(HoverButton, { onClick: openTelegramOAuth, children: [
1106
+ /* @__PURE__ */ jsx5(TelegramColorIcon, { style: iconSize }),
737
1107
  "Telegram"
738
1108
  ] })
739
1109
  }
740
1110
  ),
741
- /* @__PURE__ */ jsx4("div", { style: dividerStyle }),
1111
+ /* @__PURE__ */ jsx5("div", { style: dividerStyle }),
742
1112
  /* @__PURE__ */ jsxs3(
743
1113
  HoverButton,
744
1114
  {
@@ -747,14 +1117,14 @@ function LoginModal({ onConnectWallet, walletIcon }) {
747
1117
  onConnectWallet();
748
1118
  },
749
1119
  children: [
750
- walletIcon ?? /* @__PURE__ */ jsx4(MetaMaskColorIcon, { style: iconSize }),
1120
+ walletIcon ?? /* @__PURE__ */ jsx5(MetaMaskColorIcon, { style: iconSize }),
751
1121
  "Wallet"
752
1122
  ]
753
1123
  }
754
1124
  )
755
1125
  ] });
756
- return /* @__PURE__ */ jsx4(AnimatePresence, { children: loginModalOpen && (isMobile ? /* @__PURE__ */ jsx4(MobileDrawer, { onClose: closeLoginModal, children: modalContent }, "drawer") : /* @__PURE__ */ jsxs3(Fragment, { children: [
757
- /* @__PURE__ */ jsx4(
1126
+ return /* @__PURE__ */ jsx5(AnimatePresence, { children: loginModalOpen && (isMobile ? /* @__PURE__ */ jsx5(MobileDrawer, { onClose: closeLoginModal, children: modalContent }, "drawer") : /* @__PURE__ */ jsxs3(Fragment, { children: [
1127
+ /* @__PURE__ */ jsx5(
758
1128
  motion.div,
759
1129
  {
760
1130
  style: backdropStyle,
@@ -766,7 +1136,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
766
1136
  },
767
1137
  "backdrop"
768
1138
  ),
769
- /* @__PURE__ */ jsx4(
1139
+ /* @__PURE__ */ jsx5(
770
1140
  motion.div,
771
1141
  {
772
1142
  style: modalWrapperStyle,
@@ -785,7 +1155,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
785
1155
  transition: { duration: 0.2, ease: "easeOut" },
786
1156
  onClick: (e) => e.stopPropagation(),
787
1157
  children: [
788
- /* @__PURE__ */ jsx4("p", { style: headingStyle, children: "Connect" }),
1158
+ /* @__PURE__ */ jsx5("p", { style: headingStyle, children: "Connect" }),
789
1159
  modalContent
790
1160
  ]
791
1161
  }
@@ -852,7 +1222,7 @@ function MobileDrawer({
852
1222
  [onClose, controls]
853
1223
  );
854
1224
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
855
- /* @__PURE__ */ jsx4(
1225
+ /* @__PURE__ */ jsx5(
856
1226
  motion.div,
857
1227
  {
858
1228
  style: backdropStyle,
@@ -877,9 +1247,9 @@ function MobileDrawer({
877
1247
  dragElastic: { top: 0, bottom: 0.4 },
878
1248
  onDragEnd: handleDragEnd,
879
1249
  children: [
880
- /* @__PURE__ */ jsx4("div", { style: drawerBgStyle }),
881
- /* @__PURE__ */ jsx4("div", { style: grabHandleAreaStyle, children: /* @__PURE__ */ jsx4("div", { style: grabHandleStyle }) }),
882
- /* @__PURE__ */ jsx4("p", { style: headingStyle, children: "Connect" }),
1250
+ /* @__PURE__ */ jsx5("div", { style: drawerBgStyle }),
1251
+ /* @__PURE__ */ jsx5("div", { style: grabHandleAreaStyle, children: /* @__PURE__ */ jsx5("div", { style: grabHandleStyle }) }),
1252
+ /* @__PURE__ */ jsx5("p", { style: headingStyle, children: "Connect" }),
883
1253
  children
884
1254
  ]
885
1255
  },
@@ -887,10 +1257,21 @@ function MobileDrawer({
887
1257
  )
888
1258
  ] });
889
1259
  }
1260
+
1261
+ // src/types.ts
1262
+ function createEoaSigner(signTypedDataAsync, chainId) {
1263
+ const resolve = typeof signTypedDataAsync === "function" ? signTypedDataAsync : (args) => signTypedDataAsync.current(args);
1264
+ return {
1265
+ signTypedData: (params) => resolve(params),
1266
+ chainId
1267
+ };
1268
+ }
890
1269
  export {
891
1270
  GrpcExchangeTransport,
892
1271
  HypurrConnectProvider,
893
1272
  LoginModal,
1273
+ TelegramLoginWidget,
1274
+ createEoaSigner,
894
1275
  createStaticClient,
895
1276
  createTelegramClient,
896
1277
  useHypurrConnect