@hfunlabs/hypurr-connect 0.1.1 → 0.1.2

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,9 +1,9 @@
1
1
  // src/HypurrConnectProvider.tsx
2
2
  import {
3
3
  ExchangeClient,
4
- HttpTransport,
5
- InfoClient
4
+ HttpTransport
6
5
  } from "@hfunlabs/hyperliquid";
6
+ import { approveAgent as sdkApproveAgent } from "@hfunlabs/hyperliquid/api/exchange";
7
7
  import { PrivateKeySigner } from "@hfunlabs/hyperliquid/signing";
8
8
  import {
9
9
  createContext,
@@ -11,10 +11,12 @@ import {
11
11
  useContext,
12
12
  useEffect,
13
13
  useMemo,
14
+ useRef,
14
15
  useState
15
16
  } from "react";
16
17
 
17
18
  // src/agent.ts
19
+ var AGENT_NAME = "hypurr-connect";
18
20
  var AGENT_STORAGE_PREFIX = "hypurr-connect-agent";
19
21
  function storageKey(masterAddress) {
20
22
  return `${AGENT_STORAGE_PREFIX}:${masterAddress.toLowerCase()}`;
@@ -41,6 +43,39 @@ async function generateAgentKey() {
41
43
  const signer = new PrivateKeySigner2(privateKey);
42
44
  return { privateKey, address: signer.address };
43
45
  }
46
+ async function fetchActiveAgent(userAddress, isTestnet) {
47
+ const url = isTestnet ? "https://api.hyperliquid-testnet.xyz/info" : "https://api.hyperliquid.xyz/info";
48
+ const res = await fetch(url, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({ type: "extraAgents", user: userAddress })
52
+ });
53
+ if (!res.ok) return null;
54
+ const agents = await res.json();
55
+ if (!Array.isArray(agents)) return null;
56
+ const nowMs = Date.now();
57
+ const match = agents.find(
58
+ (a) => a.name === AGENT_NAME && a.validUntil * 1e3 > nowMs
59
+ );
60
+ if (!match) return null;
61
+ return { ...match, validUntil: match.validUntil * 1e3 };
62
+ }
63
+ async function isAgentValid(stored, userAddress, isTestnet) {
64
+ if (stored.validUntil <= Date.now()) return false;
65
+ const remote = await fetchActiveAgent(userAddress, isTestnet);
66
+ if (!remote) return false;
67
+ return remote.address.toLowerCase() === stored.address.toLowerCase() && remote.validUntil > Date.now();
68
+ }
69
+ var DEAD_AGENT_PATTERNS = [
70
+ /agent address .+ is not valid/i,
71
+ /unknown signer/i,
72
+ /not authorized/i,
73
+ /not an agent/i
74
+ ];
75
+ function isDeadAgentError(err) {
76
+ const msg = err instanceof Error ? err.message : typeof err === "object" && err !== null && "message" in err ? String(err.message) : String(err);
77
+ return DEAD_AGENT_PATTERNS.some((p) => p.test(msg));
78
+ }
44
79
 
45
80
  // src/grpc.ts
46
81
  import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
@@ -152,6 +187,14 @@ function useHypurrConnect() {
152
187
  );
153
188
  return ctx;
154
189
  }
190
+ function useHypurrConnectInternal() {
191
+ const ctx = useContext(HypurrConnectContext);
192
+ if (!ctx)
193
+ throw new Error(
194
+ "useHypurrConnectInternal must be used within <HypurrConnectProvider>"
195
+ );
196
+ return ctx;
197
+ }
155
198
  function HypurrConnectProvider({
156
199
  config,
157
200
  children
@@ -175,6 +218,7 @@ function HypurrConnectProvider({
175
218
  () => tgLoginData ? toAuthDataMap(tgLoginData) : {},
176
219
  [tgLoginData]
177
220
  );
221
+ const [tgUserTick, setTgUserTick] = useState(0);
178
222
  useEffect(() => {
179
223
  if (!tgLoginData) return;
180
224
  let cancelled = false;
@@ -183,9 +227,7 @@ function HypurrConnectProvider({
183
227
  (async () => {
184
228
  try {
185
229
  const authData = toAuthDataMap(tgLoginData);
186
- console.log(authData);
187
230
  const { response } = await tgClient.telegramUser({ authData });
188
- console.log(response);
189
231
  if (cancelled) return;
190
232
  setTgUser(response.user ?? null);
191
233
  } catch (err) {
@@ -199,27 +241,55 @@ function HypurrConnectProvider({
199
241
  return () => {
200
242
  cancelled = true;
201
243
  };
202
- }, [tgLoginData, tgClient]);
244
+ }, [tgLoginData, tgClient, tgUserTick]);
203
245
  const [eoaAddress, setEoaAddress] = useState(null);
204
246
  const [agent, setAgent] = useState(null);
247
+ const [eoaLoading, setEoaLoading] = useState(false);
248
+ const [eoaError, setEoaError] = useState(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,16 @@ 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 agentReady = authMethod === "telegram" || authMethod === "eoa" && !!agent;
235
314
  const exchange = useMemo(() => {
236
315
  if (authMethod === "telegram" && user?.address) {
237
316
  const transport = new GrpcExchangeTransport({
@@ -246,127 +325,129 @@ function HypurrConnectProvider({
246
325
  userAddress: user.address
247
326
  });
248
327
  }
249
- if (authMethod === "eoa" && agent) {
328
+ if (authMethod === "eoa" && eoaAddress) {
329
+ if (!agent) {
330
+ const noAgentTransport = {
331
+ isTestnet: config.isTestnet ?? false,
332
+ request() {
333
+ throw new Error(
334
+ "[HypurrConnect] No agent key approved. Call approveAgent(signTypedDataAsync) before using the exchange client. This is required for EOA wallets to sign transactions on Hyperliquid."
335
+ );
336
+ }
337
+ };
338
+ return new ExchangeClient({
339
+ transport: noAgentTransport,
340
+ externalSigning: true,
341
+ userAddress: eoaAddress
342
+ });
343
+ }
344
+ const inner = new HttpTransport({
345
+ isTestnet: config.isTestnet ?? false
346
+ });
347
+ const deadAgentAddr = eoaAddress;
348
+ const guardedTransport = {
349
+ isTestnet: inner.isTestnet,
350
+ async request(endpoint, payload, signal) {
351
+ try {
352
+ return await inner.request(endpoint, payload, signal);
353
+ } catch (err) {
354
+ if (endpoint === "exchange" && isDeadAgentError(err)) {
355
+ onDeadAgentRef.current?.(deadAgentAddr);
356
+ }
357
+ throw err;
358
+ }
359
+ }
360
+ };
250
361
  const wallet = new PrivateKeySigner(agent.privateKey);
251
362
  return new ExchangeClient({
252
- transport: new HttpTransport({
253
- isTestnet: config.isTestnet ?? false
254
- }),
363
+ transport: guardedTransport,
255
364
  wallet
256
365
  });
257
366
  }
258
367
  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
284
- });
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
- }
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
- 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
312
- };
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
- ]
320
- };
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)
335
- }
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
- };
359
- saveAgent(eoaAddress, stored);
360
- setAgent(stored);
361
- },
362
- [eoaAddress, config.isTestnet]
363
- );
368
+ }, [
369
+ authMethod,
370
+ user,
371
+ agent,
372
+ eoaAddress,
373
+ config.isTestnet,
374
+ tgClient,
375
+ authDataMap
376
+ ]);
364
377
  const handleClearAgent = useCallback(() => {
365
378
  if (eoaAddress) {
366
379
  clearAgent(eoaAddress);
367
380
  setAgent(null);
368
381
  }
369
382
  }, [eoaAddress]);
383
+ const createWallet = useCallback(
384
+ async (name) => {
385
+ const { response } = await tgClient.hyperliquidWalletCreate({
386
+ authData: authDataMap,
387
+ name
388
+ });
389
+ refreshWallets();
390
+ if (!response.wallet)
391
+ throw new Error("Wallet creation returned no wallet");
392
+ return response.wallet;
393
+ },
394
+ [tgClient, authDataMap, refreshWallets]
395
+ );
396
+ const deleteWallet = useCallback(
397
+ async (walletId) => {
398
+ await tgClient.hyperliquidWalletDelete({
399
+ authData: authDataMap,
400
+ walletId
401
+ });
402
+ if (walletId === selectedWalletId) {
403
+ const remaining = wallets.filter((w) => w.id !== walletId);
404
+ setSelectedWalletId(remaining[0]?.id ?? 0);
405
+ }
406
+ refreshWallets();
407
+ },
408
+ [tgClient, authDataMap, selectedWalletId, wallets, refreshWallets]
409
+ );
410
+ const createWalletPack = useCallback(
411
+ async (name) => {
412
+ const { response } = await tgClient.telegramChatWalletPackCreate({
413
+ authData: authDataMap,
414
+ name
415
+ });
416
+ refreshWallets();
417
+ return response.packId;
418
+ },
419
+ [tgClient, authDataMap, refreshWallets]
420
+ );
421
+ const addPackLabel = useCallback(
422
+ async (params) => {
423
+ await tgClient.telegramChatWalletPackLabelAdd({
424
+ authData: authDataMap,
425
+ ...params
426
+ });
427
+ refreshWallets();
428
+ },
429
+ [tgClient, authDataMap, refreshWallets]
430
+ );
431
+ const modifyPackLabel = useCallback(
432
+ async (params) => {
433
+ await tgClient.telegramChatWalletPackLabelModify({
434
+ authData: authDataMap,
435
+ ...params
436
+ });
437
+ refreshWallets();
438
+ },
439
+ [tgClient, authDataMap, refreshWallets]
440
+ );
441
+ const removePackLabel = useCallback(
442
+ async (params) => {
443
+ await tgClient.telegramChatWalletPackLabelRemove({
444
+ authData: authDataMap,
445
+ ...params
446
+ });
447
+ refreshWallets();
448
+ },
449
+ [tgClient, authDataMap, refreshWallets]
450
+ );
370
451
  const [loginModalOpen, setLoginModalOpen] = useState(false);
371
452
  const openLoginModal = useCallback(() => setLoginModalOpen(true), []);
372
453
  const closeLoginModal = useCallback(() => setLoginModalOpen(false), []);
@@ -375,42 +456,115 @@ function HypurrConnectProvider({
375
456
  localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(data));
376
457
  setEoaAddress(null);
377
458
  setAgent(null);
459
+ setEoaError(null);
378
460
  }, []);
379
- const loginEoa = useCallback((address) => {
461
+ const connectEoa = useCallback((address) => {
380
462
  setEoaAddress(address);
381
463
  setTgLoginData(null);
382
464
  setTgUser(null);
383
465
  setTgError(null);
466
+ setEoaError(null);
384
467
  localStorage.removeItem(TELEGRAM_STORAGE_KEY);
468
+ const existing = loadAgent(address);
469
+ if (existing && existing.validUntil > Date.now()) {
470
+ setAgent(existing);
471
+ } else {
472
+ if (existing) clearAgent(address);
473
+ setAgent(null);
474
+ }
385
475
  }, []);
476
+ const approveAgentFn = useCallback(
477
+ async (signTypedDataAsync, chainId) => {
478
+ if (!eoaAddress) {
479
+ throw new Error(
480
+ "[HypurrConnect] Cannot approve agent: no EOA wallet connected. Call connectEoa(address) first."
481
+ );
482
+ }
483
+ setEoaLoading(true);
484
+ setEoaError(null);
485
+ try {
486
+ const existing = loadAgent(eoaAddress);
487
+ if (existing) {
488
+ const isTestnet2 = config.isTestnet ?? false;
489
+ const valid = await isAgentValid(existing, eoaAddress, isTestnet2);
490
+ if (valid) {
491
+ setAgent(existing);
492
+ return;
493
+ }
494
+ clearAgent(eoaAddress);
495
+ }
496
+ const { privateKey, address: agentAddress } = await generateAgentKey();
497
+ const isTestnet = config.isTestnet ?? false;
498
+ const wallet = {
499
+ signTypedData: signTypedDataAsync,
500
+ getAddresses: async () => [eoaAddress],
501
+ getChainId: async () => chainId
502
+ };
503
+ const transport = new HttpTransport({ isTestnet });
504
+ await sdkApproveAgent(
505
+ { transport, wallet },
506
+ {
507
+ agentAddress: agentAddress.toLowerCase(),
508
+ agentName: AGENT_NAME
509
+ }
510
+ );
511
+ const remote = await fetchActiveAgent(eoaAddress, isTestnet);
512
+ const validUntil = remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1e3;
513
+ const stored = {
514
+ privateKey,
515
+ address: agentAddress,
516
+ approvedAt: Date.now(),
517
+ validUntil
518
+ };
519
+ saveAgent(eoaAddress, stored);
520
+ setAgent(stored);
521
+ } catch (err) {
522
+ console.error("[HypurrConnect] EOA agent approval failed:", err);
523
+ setEoaError(err instanceof Error ? err.message : String(err));
524
+ setAgent(null);
525
+ } finally {
526
+ setEoaLoading(false);
527
+ }
528
+ },
529
+ [eoaAddress, config.isTestnet]
530
+ );
386
531
  const logout = useCallback(() => {
387
532
  setTgLoginData(null);
388
533
  setTgUser(null);
389
534
  setTgError(null);
390
535
  setEoaAddress(null);
391
536
  setAgent(null);
537
+ setEoaError(null);
392
538
  localStorage.removeItem(TELEGRAM_STORAGE_KEY);
393
539
  }, []);
394
540
  const value = useMemo(
395
541
  () => ({
396
542
  user,
397
543
  isLoggedIn: !!user,
398
- isLoading: tgLoading,
399
- error: tgError,
544
+ isLoading: tgLoading || eoaLoading,
545
+ error: tgError ?? eoaError,
400
546
  authMethod,
401
547
  exchange,
402
- usdcBalance,
403
- usdcBalanceLoading,
404
- refreshBalance,
548
+ wallets,
549
+ selectedWalletId,
550
+ selectWallet,
551
+ createWallet,
552
+ deleteWallet,
553
+ refreshWallets,
554
+ packs,
555
+ createWalletPack,
556
+ addPackLabel,
557
+ modifyPackLabel,
558
+ removePackLabel,
405
559
  loginModalOpen,
406
560
  openLoginModal,
407
561
  closeLoginModal,
408
562
  loginTelegram,
409
- loginEoa,
563
+ connectEoa,
564
+ approveAgent: approveAgentFn,
410
565
  logout,
411
566
  agent,
412
- agentReady: authMethod === "telegram" || !!agent,
413
- approveAgent,
567
+ agentReady,
414
568
  clearAgent: handleClearAgent,
415
569
  botId: config.telegram?.botId ?? "",
416
570
  authDataMap,
@@ -420,20 +574,31 @@ function HypurrConnectProvider({
420
574
  [
421
575
  user,
422
576
  tgLoading,
577
+ eoaLoading,
423
578
  tgError,
579
+ eoaError,
424
580
  authMethod,
425
581
  exchange,
426
- usdcBalance,
427
- usdcBalanceLoading,
428
- refreshBalance,
582
+ wallets,
583
+ selectedWalletId,
584
+ selectWallet,
585
+ createWallet,
586
+ deleteWallet,
587
+ refreshWallets,
588
+ packs,
589
+ createWalletPack,
590
+ addPackLabel,
591
+ modifyPackLabel,
592
+ removePackLabel,
429
593
  loginModalOpen,
430
594
  openLoginModal,
431
595
  closeLoginModal,
432
596
  loginTelegram,
433
- loginEoa,
597
+ connectEoa,
598
+ approveAgentFn,
434
599
  logout,
435
600
  agent,
436
- approveAgent,
601
+ agentReady,
437
602
  handleClearAgent,
438
603
  config.telegram?.botId,
439
604
  authDataMap,
@@ -674,7 +839,7 @@ function HoverButton({
674
839
  );
675
840
  }
676
841
  function LoginModal({ onConnectWallet, walletIcon }) {
677
- const { loginTelegram, loginModalOpen, closeLoginModal, botId } = useHypurrConnect();
842
+ const { loginTelegram, loginModalOpen, closeLoginModal, botId } = useHypurrConnectInternal();
678
843
  const handleTelegramAuth = useCallback2(
679
844
  (user) => {
680
845
  loginTelegram(user);