@hfunlabs/hypurr-connect 0.1.2 → 0.1.4

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
@@ -3,8 +3,7 @@ import {
3
3
  ExchangeClient,
4
4
  HttpTransport
5
5
  } from "@hfunlabs/hyperliquid";
6
- import { approveAgent as sdkApproveAgent } from "@hfunlabs/hyperliquid/api/exchange";
7
- import { PrivateKeySigner } from "@hfunlabs/hyperliquid/signing";
6
+ import { PrivateKeySigner, signUserSignedAction } from "@hfunlabs/hyperliquid/signing";
8
7
  import {
9
8
  createContext,
10
9
  useCallback,
@@ -227,9 +226,16 @@ function HypurrConnectProvider({
227
226
  (async () => {
228
227
  try {
229
228
  const authData = toAuthDataMap(tgLoginData);
230
- const { response } = await tgClient.telegramUser({ authData });
229
+ const [{ response: userResp }, { response: walletsResp }] = await Promise.all([
230
+ tgClient.telegramUser({ authData }),
231
+ tgClient.telegramUserWallets({ authData })
232
+ ]);
231
233
  if (cancelled) return;
232
- setTgUser(response.user ?? null);
234
+ const user2 = userResp.user ?? null;
235
+ if (user2) {
236
+ user2.wallets = walletsResp.wallets;
237
+ }
238
+ setTgUser(user2);
233
239
  } catch (err) {
234
240
  if (cancelled) return;
235
241
  console.error("[HypurrConnect] gRPC TelegramUser failed:", err);
@@ -246,6 +252,7 @@ function HypurrConnectProvider({
246
252
  const [agent, setAgent] = useState(null);
247
253
  const [eoaLoading, setEoaLoading] = useState(false);
248
254
  const [eoaError, setEoaError] = useState(null);
255
+ const eoaSignerRef = useRef(null);
249
256
  const authMethod = tgLoginData ? "telegram" : eoaAddress ? "eoa" : null;
250
257
  const [wallets, setWallets] = useState([]);
251
258
  const [selectedWalletId, setSelectedWalletId] = useState(0);
@@ -310,6 +317,13 @@ function HypurrConnectProvider({
310
317
  setAgent(null);
311
318
  setEoaError("Agent expired or was deregistered. Please reconnect.");
312
319
  };
320
+ const agentSignerRef = useRef(
321
+ agent ? new PrivateKeySigner(agent.privateKey) : null
322
+ );
323
+ useEffect(() => {
324
+ agentSignerRef.current = agent ? new PrivateKeySigner(agent.privateKey) : null;
325
+ }, [agent]);
326
+ const provisioningRef = useRef(null);
313
327
  const agentReady = authMethod === "telegram" || authMethod === "eoa" && !!agent;
314
328
  const exchange = useMemo(() => {
315
329
  if (authMethod === "telegram" && user?.address) {
@@ -326,12 +340,13 @@ function HypurrConnectProvider({
326
340
  });
327
341
  }
328
342
  if (authMethod === "eoa" && eoaAddress) {
329
- if (!agent) {
343
+ const hasSigner = !!eoaSignerRef.current;
344
+ if (!agent && !hasSigner) {
330
345
  const noAgentTransport = {
331
346
  isTestnet: config.isTestnet ?? false,
332
347
  request() {
333
348
  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."
349
+ "[HypurrConnect] No agent key approved and no wallet signer available. Either call approveAgent(signTypedDataAsync) or pass a signer to connectEoa(address, { signTypedData, chainId })."
335
350
  );
336
351
  }
337
352
  };
@@ -341,9 +356,8 @@ function HypurrConnectProvider({
341
356
  userAddress: eoaAddress
342
357
  });
343
358
  }
344
- const inner = new HttpTransport({
345
- isTestnet: config.isTestnet ?? false
346
- });
359
+ const isTestnet = config.isTestnet ?? false;
360
+ const inner = new HttpTransport({ isTestnet });
347
361
  const deadAgentAddr = eoaAddress;
348
362
  const guardedTransport = {
349
363
  isTestnet: inner.isTestnet,
@@ -358,10 +372,110 @@ function HypurrConnectProvider({
358
372
  }
359
373
  }
360
374
  };
361
- const wallet = new PrivateKeySigner(agent.privateKey);
375
+ const signerRef = eoaSignerRef;
376
+ const agentRef = agentSignerRef;
377
+ const provRef = provisioningRef;
378
+ const ownerAddress = eoaAddress;
379
+ const ensureAgent = async () => {
380
+ const existing = agentRef.current;
381
+ if (existing) return existing;
382
+ if (provRef.current) return provRef.current;
383
+ const signer = signerRef.current;
384
+ if (!signer) {
385
+ throw new Error(
386
+ "[HypurrConnect] No wallet signer available to auto-provision agent. Pass a signer to connectEoa(address, { signTypedData, chainId })."
387
+ );
388
+ }
389
+ provRef.current = (async () => {
390
+ try {
391
+ const { privateKey, address: agentAddress } = await generateAgentKey();
392
+ const chainIdHex = `0x${signer.chainId.toString(16)}`;
393
+ const nonce = Date.now();
394
+ const action = {
395
+ type: "approveAgent",
396
+ signatureChainId: chainIdHex,
397
+ hyperliquidChain: isTestnet ? "Testnet" : "Mainnet",
398
+ agentAddress: agentAddress.toLowerCase(),
399
+ agentName: AGENT_NAME,
400
+ nonce
401
+ };
402
+ const approveAgentTypes = {
403
+ "HyperliquidTransaction:ApproveAgent": [
404
+ { name: "hyperliquidChain", type: "string" },
405
+ { name: "agentAddress", type: "address" },
406
+ { name: "agentName", type: "string" },
407
+ { name: "nonce", type: "uint64" }
408
+ ]
409
+ };
410
+ const wallet = {
411
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
412
+ signTypedData(params) {
413
+ return signer.signTypedData(params);
414
+ },
415
+ getAddresses: async () => [ownerAddress],
416
+ getChainId: async () => signer.chainId
417
+ };
418
+ const signature = await signUserSignedAction({
419
+ wallet,
420
+ action,
421
+ types: approveAgentTypes
422
+ });
423
+ const apiUrl = isTestnet ? "https://api.hyperliquid-testnet.xyz/exchange" : "https://api.hyperliquid.xyz/exchange";
424
+ const res = await fetch(apiUrl, {
425
+ method: "POST",
426
+ headers: { "Content-Type": "application/json" },
427
+ body: JSON.stringify({ action, signature, nonce })
428
+ });
429
+ const body = await res.json();
430
+ if (body?.status === "err") {
431
+ throw new Error(
432
+ `approveAgent API error: ${body.response ?? JSON.stringify(body)}`
433
+ );
434
+ }
435
+ const remote = await fetchActiveAgent(ownerAddress, isTestnet);
436
+ const validUntil = remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1e3;
437
+ const stored = {
438
+ privateKey,
439
+ address: agentAddress,
440
+ approvedAt: Date.now(),
441
+ validUntil
442
+ };
443
+ saveAgent(ownerAddress, stored);
444
+ const newSigner = new PrivateKeySigner(privateKey);
445
+ agentRef.current = newSigner;
446
+ setAgent(stored);
447
+ return newSigner;
448
+ } finally {
449
+ provRef.current = null;
450
+ }
451
+ })();
452
+ return provRef.current;
453
+ };
454
+ const dualWallet = {
455
+ address: ownerAddress,
456
+ async signTypedData(params) {
457
+ if (params.domain.name === "HyperliquidSignTransaction") {
458
+ const signer = signerRef.current;
459
+ if (!signer) {
460
+ throw new Error(
461
+ "[HypurrConnect] No wallet signer available for user-signed actions. Pass a signer to connectEoa(address, { signTypedData, chainId })."
462
+ );
463
+ }
464
+ return signer.signTypedData(
465
+ params
466
+ );
467
+ }
468
+ const agentSigner = await ensureAgent();
469
+ return agentSigner.signTypedData(params);
470
+ }
471
+ };
362
472
  return new ExchangeClient({
363
473
  transport: guardedTransport,
364
- wallet
474
+ wallet: dualWallet,
475
+ signatureChainId: () => {
476
+ const id = signerRef.current?.chainId ?? 42161;
477
+ return `0x${id.toString(16)}`;
478
+ }
365
479
  });
366
480
  }
367
481
  return null;
@@ -458,21 +572,25 @@ function HypurrConnectProvider({
458
572
  setAgent(null);
459
573
  setEoaError(null);
460
574
  }, []);
461
- const connectEoa = useCallback((address) => {
462
- setEoaAddress(address);
463
- setTgLoginData(null);
464
- setTgUser(null);
465
- setTgError(null);
466
- setEoaError(null);
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
- }
475
- }, []);
575
+ const connectEoa = useCallback(
576
+ (address, signer) => {
577
+ eoaSignerRef.current = signer ?? null;
578
+ setEoaAddress(address);
579
+ setTgLoginData(null);
580
+ setTgUser(null);
581
+ setTgError(null);
582
+ setEoaError(null);
583
+ localStorage.removeItem(TELEGRAM_STORAGE_KEY);
584
+ const existing = loadAgent(address);
585
+ if (existing && existing.validUntil > Date.now()) {
586
+ setAgent(existing);
587
+ } else {
588
+ if (existing) clearAgent(address);
589
+ setAgent(null);
590
+ }
591
+ },
592
+ []
593
+ );
476
594
  const approveAgentFn = useCallback(
477
595
  async (signTypedDataAsync, chainId) => {
478
596
  if (!eoaAddress) {
@@ -480,6 +598,7 @@ function HypurrConnectProvider({
480
598
  "[HypurrConnect] Cannot approve agent: no EOA wallet connected. Call connectEoa(address) first."
481
599
  );
482
600
  }
601
+ eoaSignerRef.current = { signTypedData: signTypedDataAsync, chainId };
483
602
  setEoaLoading(true);
484
603
  setEoaError(null);
485
604
  try {
@@ -495,19 +614,49 @@ function HypurrConnectProvider({
495
614
  }
496
615
  const { privateKey, address: agentAddress } = await generateAgentKey();
497
616
  const isTestnet = config.isTestnet ?? false;
617
+ const chainIdHex = `0x${chainId.toString(16)}`;
618
+ const nonce = Date.now();
619
+ const action = {
620
+ type: "approveAgent",
621
+ signatureChainId: chainIdHex,
622
+ hyperliquidChain: isTestnet ? "Testnet" : "Mainnet",
623
+ agentAddress: agentAddress.toLowerCase(),
624
+ agentName: AGENT_NAME,
625
+ nonce
626
+ };
627
+ const approveAgentTypes = {
628
+ "HyperliquidTransaction:ApproveAgent": [
629
+ { name: "hyperliquidChain", type: "string" },
630
+ { name: "agentAddress", type: "address" },
631
+ { name: "agentName", type: "string" },
632
+ { name: "nonce", type: "uint64" }
633
+ ]
634
+ };
498
635
  const wallet = {
499
- signTypedData: signTypedDataAsync,
636
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
637
+ signTypedData(params) {
638
+ return signTypedDataAsync(params);
639
+ },
500
640
  getAddresses: async () => [eoaAddress],
501
641
  getChainId: async () => chainId
502
642
  };
503
- const transport = new HttpTransport({ isTestnet });
504
- await sdkApproveAgent(
505
- { transport, wallet },
506
- {
507
- agentAddress: agentAddress.toLowerCase(),
508
- agentName: AGENT_NAME
509
- }
510
- );
643
+ const signature = await signUserSignedAction({
644
+ wallet,
645
+ action,
646
+ types: approveAgentTypes
647
+ });
648
+ const apiUrl = isTestnet ? "https://api.hyperliquid-testnet.xyz/exchange" : "https://api.hyperliquid.xyz/exchange";
649
+ const res = await fetch(apiUrl, {
650
+ method: "POST",
651
+ headers: { "Content-Type": "application/json" },
652
+ body: JSON.stringify({ action, signature, nonce })
653
+ });
654
+ const body = await res.json();
655
+ if (body?.status === "err") {
656
+ throw new Error(
657
+ `approveAgent API error: ${body.response ?? JSON.stringify(body)}`
658
+ );
659
+ }
511
660
  const remote = await fetchActiveAgent(eoaAddress, isTestnet);
512
661
  const validUntil = remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1e3;
513
662
  const stored = {
@@ -535,6 +684,7 @@ function HypurrConnectProvider({
535
684
  setEoaAddress(null);
536
685
  setAgent(null);
537
686
  setEoaError(null);
687
+ eoaSignerRef.current = null;
538
688
  localStorage.removeItem(TELEGRAM_STORAGE_KEY);
539
689
  }, []);
540
690
  const value = useMemo(
@@ -567,6 +717,8 @@ function HypurrConnectProvider({
567
717
  agentReady,
568
718
  clearAgent: handleClearAgent,
569
719
  botId: config.telegram?.botId ?? "",
720
+ botUsername: config.telegram?.botUsername ?? "",
721
+ useWidget: config.telegram?.useWidget ?? false,
570
722
  authDataMap,
571
723
  telegramClient: tgClient,
572
724
  staticClient
@@ -601,6 +753,8 @@ function HypurrConnectProvider({
601
753
  agentReady,
602
754
  handleClearAgent,
603
755
  config.telegram?.botId,
756
+ config.telegram?.botUsername,
757
+ config.telegram?.useWidget,
604
758
  authDataMap,
605
759
  tgClient,
606
760
  staticClient
@@ -617,7 +771,7 @@ import {
617
771
  } from "framer-motion";
618
772
  import {
619
773
  useCallback as useCallback2,
620
- useEffect as useEffect2,
774
+ useEffect as useEffect3,
621
775
  useSyncExternalStore
622
776
  } from "react";
623
777
 
@@ -748,8 +902,53 @@ function TelegramColorIcon({ style }) {
748
902
  );
749
903
  }
750
904
 
905
+ // src/TelegramLoginWidget.tsx
906
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
907
+ import { jsx as jsx4 } from "react/jsx-runtime";
908
+ var WIDGET_SCRIPT_URL = "https://telegram.org/js/telegram-widget.js?22";
909
+ var CALLBACK_NAME = "__hypurrConnectTelegramAuth";
910
+ function TelegramLoginWidget({
911
+ botUsername,
912
+ onAuth,
913
+ buttonSize = "large",
914
+ cornerRadius,
915
+ showUserPhoto = true,
916
+ requestAccess = true
917
+ }) {
918
+ const containerRef = useRef2(null);
919
+ const onAuthRef = useRef2(onAuth);
920
+ onAuthRef.current = onAuth;
921
+ useEffect2(() => {
922
+ const container = containerRef.current;
923
+ if (!container) return;
924
+ window[CALLBACK_NAME] = (user) => {
925
+ onAuthRef.current(user);
926
+ };
927
+ const script = document.createElement("script");
928
+ script.src = WIDGET_SCRIPT_URL;
929
+ script.async = true;
930
+ script.setAttribute("data-telegram-login", botUsername);
931
+ script.setAttribute("data-size", buttonSize);
932
+ script.setAttribute("data-onauth", `${CALLBACK_NAME}(user)`);
933
+ script.setAttribute("data-userpic", String(showUserPhoto));
934
+ if (requestAccess) {
935
+ script.setAttribute("data-request-access", "write");
936
+ }
937
+ if (cornerRadius !== void 0) {
938
+ script.setAttribute("data-radius", String(cornerRadius));
939
+ }
940
+ container.innerHTML = "";
941
+ container.appendChild(script);
942
+ return () => {
943
+ container.innerHTML = "";
944
+ delete window[CALLBACK_NAME];
945
+ };
946
+ }, [botUsername, buttonSize, cornerRadius, showUserPhoto, requestAccess]);
947
+ return /* @__PURE__ */ jsx4("div", { ref: containerRef });
948
+ }
949
+
751
950
  // src/LoginModal.tsx
752
- import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
951
+ import { Fragment, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
753
952
  var MOBILE_BREAKPOINT = 640;
754
953
  var btnStyle = {
755
954
  display: "flex",
@@ -827,7 +1026,7 @@ function HoverButton({
827
1026
  onClick,
828
1027
  children
829
1028
  }) {
830
- return /* @__PURE__ */ jsx4(
1029
+ return /* @__PURE__ */ jsx5(
831
1030
  motion.button,
832
1031
  {
833
1032
  type: "button",
@@ -839,7 +1038,14 @@ function HoverButton({
839
1038
  );
840
1039
  }
841
1040
  function LoginModal({ onConnectWallet, walletIcon }) {
842
- const { loginTelegram, loginModalOpen, closeLoginModal, botId } = useHypurrConnectInternal();
1041
+ const {
1042
+ loginTelegram,
1043
+ loginModalOpen,
1044
+ closeLoginModal,
1045
+ botId,
1046
+ botUsername,
1047
+ useWidget
1048
+ } = useHypurrConnectInternal();
843
1049
  const handleTelegramAuth = useCallback2(
844
1050
  (user) => {
845
1051
  loginTelegram(user);
@@ -847,7 +1053,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
847
1053
  },
848
1054
  [loginTelegram, closeLoginModal]
849
1055
  );
850
- useEffect2(() => {
1056
+ useEffect3(() => {
851
1057
  if (!loginModalOpen) return;
852
1058
  function onMessage(e) {
853
1059
  if (e.origin !== "https://oauth.telegram.org") return;
@@ -886,7 +1092,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
886
1092
  }, [botId]);
887
1093
  const isMobile = useIsMobile();
888
1094
  const modalContent = /* @__PURE__ */ jsxs3(Fragment, { children: [
889
- /* @__PURE__ */ jsx4(
1095
+ /* @__PURE__ */ jsx5(
890
1096
  "div",
891
1097
  {
892
1098
  style: {
@@ -897,13 +1103,19 @@ function LoginModal({ onConnectWallet, walletIcon }) {
897
1103
  gap: 8,
898
1104
  overflow: "hidden"
899
1105
  },
900
- children: /* @__PURE__ */ jsxs3(HoverButton, { onClick: openTelegramOAuth, children: [
901
- /* @__PURE__ */ jsx4(TelegramColorIcon, { style: iconSize }),
1106
+ children: useWidget && botUsername ? /* @__PURE__ */ jsx5(
1107
+ TelegramLoginWidget,
1108
+ {
1109
+ botUsername,
1110
+ onAuth: handleTelegramAuth
1111
+ }
1112
+ ) : /* @__PURE__ */ jsxs3(HoverButton, { onClick: openTelegramOAuth, children: [
1113
+ /* @__PURE__ */ jsx5(TelegramColorIcon, { style: iconSize }),
902
1114
  "Telegram"
903
1115
  ] })
904
1116
  }
905
1117
  ),
906
- /* @__PURE__ */ jsx4("div", { style: dividerStyle }),
1118
+ /* @__PURE__ */ jsx5("div", { style: dividerStyle }),
907
1119
  /* @__PURE__ */ jsxs3(
908
1120
  HoverButton,
909
1121
  {
@@ -912,14 +1124,14 @@ function LoginModal({ onConnectWallet, walletIcon }) {
912
1124
  onConnectWallet();
913
1125
  },
914
1126
  children: [
915
- walletIcon ?? /* @__PURE__ */ jsx4(MetaMaskColorIcon, { style: iconSize }),
1127
+ walletIcon ?? /* @__PURE__ */ jsx5(MetaMaskColorIcon, { style: iconSize }),
916
1128
  "Wallet"
917
1129
  ]
918
1130
  }
919
1131
  )
920
1132
  ] });
921
- return /* @__PURE__ */ jsx4(AnimatePresence, { children: loginModalOpen && (isMobile ? /* @__PURE__ */ jsx4(MobileDrawer, { onClose: closeLoginModal, children: modalContent }, "drawer") : /* @__PURE__ */ jsxs3(Fragment, { children: [
922
- /* @__PURE__ */ jsx4(
1133
+ return /* @__PURE__ */ jsx5(AnimatePresence, { children: loginModalOpen && (isMobile ? /* @__PURE__ */ jsx5(MobileDrawer, { onClose: closeLoginModal, children: modalContent }, "drawer") : /* @__PURE__ */ jsxs3(Fragment, { children: [
1134
+ /* @__PURE__ */ jsx5(
923
1135
  motion.div,
924
1136
  {
925
1137
  style: backdropStyle,
@@ -931,7 +1143,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
931
1143
  },
932
1144
  "backdrop"
933
1145
  ),
934
- /* @__PURE__ */ jsx4(
1146
+ /* @__PURE__ */ jsx5(
935
1147
  motion.div,
936
1148
  {
937
1149
  style: modalWrapperStyle,
@@ -950,7 +1162,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
950
1162
  transition: { duration: 0.2, ease: "easeOut" },
951
1163
  onClick: (e) => e.stopPropagation(),
952
1164
  children: [
953
- /* @__PURE__ */ jsx4("p", { style: headingStyle, children: "Connect" }),
1165
+ /* @__PURE__ */ jsx5("p", { style: headingStyle, children: "Connect" }),
954
1166
  modalContent
955
1167
  ]
956
1168
  }
@@ -1017,7 +1229,7 @@ function MobileDrawer({
1017
1229
  [onClose, controls]
1018
1230
  );
1019
1231
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
1020
- /* @__PURE__ */ jsx4(
1232
+ /* @__PURE__ */ jsx5(
1021
1233
  motion.div,
1022
1234
  {
1023
1235
  style: backdropStyle,
@@ -1042,9 +1254,9 @@ function MobileDrawer({
1042
1254
  dragElastic: { top: 0, bottom: 0.4 },
1043
1255
  onDragEnd: handleDragEnd,
1044
1256
  children: [
1045
- /* @__PURE__ */ jsx4("div", { style: drawerBgStyle }),
1046
- /* @__PURE__ */ jsx4("div", { style: grabHandleAreaStyle, children: /* @__PURE__ */ jsx4("div", { style: grabHandleStyle }) }),
1047
- /* @__PURE__ */ jsx4("p", { style: headingStyle, children: "Connect" }),
1257
+ /* @__PURE__ */ jsx5("div", { style: drawerBgStyle }),
1258
+ /* @__PURE__ */ jsx5("div", { style: grabHandleAreaStyle, children: /* @__PURE__ */ jsx5("div", { style: grabHandleStyle }) }),
1259
+ /* @__PURE__ */ jsx5("p", { style: headingStyle, children: "Connect" }),
1048
1260
  children
1049
1261
  ]
1050
1262
  },
@@ -1052,10 +1264,21 @@ function MobileDrawer({
1052
1264
  )
1053
1265
  ] });
1054
1266
  }
1267
+
1268
+ // src/types.ts
1269
+ function createEoaSigner(signTypedDataAsync, chainId) {
1270
+ const resolve = typeof signTypedDataAsync === "function" ? signTypedDataAsync : (args) => signTypedDataAsync.current(args);
1271
+ return {
1272
+ signTypedData: (params) => resolve(params),
1273
+ chainId
1274
+ };
1275
+ }
1055
1276
  export {
1056
1277
  GrpcExchangeTransport,
1057
1278
  HypurrConnectProvider,
1058
1279
  LoginModal,
1280
+ TelegramLoginWidget,
1281
+ createEoaSigner,
1059
1282
  createStaticClient,
1060
1283
  createTelegramClient,
1061
1284
  useHypurrConnect