@hfunlabs/hypurr-connect 0.1.2 → 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
@@ -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,
@@ -246,6 +245,7 @@ function HypurrConnectProvider({
246
245
  const [agent, setAgent] = useState(null);
247
246
  const [eoaLoading, setEoaLoading] = useState(false);
248
247
  const [eoaError, setEoaError] = useState(null);
248
+ const eoaSignerRef = useRef(null);
249
249
  const authMethod = tgLoginData ? "telegram" : eoaAddress ? "eoa" : null;
250
250
  const [wallets, setWallets] = useState([]);
251
251
  const [selectedWalletId, setSelectedWalletId] = useState(0);
@@ -310,6 +310,13 @@ function HypurrConnectProvider({
310
310
  setAgent(null);
311
311
  setEoaError("Agent expired or was deregistered. Please reconnect.");
312
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);
313
320
  const agentReady = authMethod === "telegram" || authMethod === "eoa" && !!agent;
314
321
  const exchange = useMemo(() => {
315
322
  if (authMethod === "telegram" && user?.address) {
@@ -326,12 +333,13 @@ function HypurrConnectProvider({
326
333
  });
327
334
  }
328
335
  if (authMethod === "eoa" && eoaAddress) {
329
- if (!agent) {
336
+ const hasSigner = !!eoaSignerRef.current;
337
+ if (!agent && !hasSigner) {
330
338
  const noAgentTransport = {
331
339
  isTestnet: config.isTestnet ?? false,
332
340
  request() {
333
341
  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."
342
+ "[HypurrConnect] No agent key approved and no wallet signer available. Either call approveAgent(signTypedDataAsync) or pass a signer to connectEoa(address, { signTypedData, chainId })."
335
343
  );
336
344
  }
337
345
  };
@@ -341,9 +349,8 @@ function HypurrConnectProvider({
341
349
  userAddress: eoaAddress
342
350
  });
343
351
  }
344
- const inner = new HttpTransport({
345
- isTestnet: config.isTestnet ?? false
346
- });
352
+ const isTestnet = config.isTestnet ?? false;
353
+ const inner = new HttpTransport({ isTestnet });
347
354
  const deadAgentAddr = eoaAddress;
348
355
  const guardedTransport = {
349
356
  isTestnet: inner.isTestnet,
@@ -358,10 +365,110 @@ function HypurrConnectProvider({
358
365
  }
359
366
  }
360
367
  };
361
- const wallet = new PrivateKeySigner(agent.privateKey);
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;
446
+ };
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);
463
+ }
464
+ };
362
465
  return new ExchangeClient({
363
466
  transport: guardedTransport,
364
- wallet
467
+ wallet: dualWallet,
468
+ signatureChainId: () => {
469
+ const id = signerRef.current?.chainId ?? 42161;
470
+ return `0x${id.toString(16)}`;
471
+ }
365
472
  });
366
473
  }
367
474
  return null;
@@ -458,21 +565,25 @@ function HypurrConnectProvider({
458
565
  setAgent(null);
459
566
  setEoaError(null);
460
567
  }, []);
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
- }, []);
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
+ );
476
587
  const approveAgentFn = useCallback(
477
588
  async (signTypedDataAsync, chainId) => {
478
589
  if (!eoaAddress) {
@@ -480,6 +591,7 @@ function HypurrConnectProvider({
480
591
  "[HypurrConnect] Cannot approve agent: no EOA wallet connected. Call connectEoa(address) first."
481
592
  );
482
593
  }
594
+ eoaSignerRef.current = { signTypedData: signTypedDataAsync, chainId };
483
595
  setEoaLoading(true);
484
596
  setEoaError(null);
485
597
  try {
@@ -495,19 +607,49 @@ function HypurrConnectProvider({
495
607
  }
496
608
  const { privateKey, address: agentAddress } = await generateAgentKey();
497
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
+ };
498
628
  const wallet = {
499
- signTypedData: signTypedDataAsync,
629
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
630
+ signTypedData(params) {
631
+ return signTypedDataAsync(params);
632
+ },
500
633
  getAddresses: async () => [eoaAddress],
501
634
  getChainId: async () => chainId
502
635
  };
503
- const transport = new HttpTransport({ isTestnet });
504
- await sdkApproveAgent(
505
- { transport, wallet },
506
- {
507
- agentAddress: agentAddress.toLowerCase(),
508
- agentName: AGENT_NAME
509
- }
510
- );
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
+ }
511
653
  const remote = await fetchActiveAgent(eoaAddress, isTestnet);
512
654
  const validUntil = remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1e3;
513
655
  const stored = {
@@ -535,6 +677,7 @@ function HypurrConnectProvider({
535
677
  setEoaAddress(null);
536
678
  setAgent(null);
537
679
  setEoaError(null);
680
+ eoaSignerRef.current = null;
538
681
  localStorage.removeItem(TELEGRAM_STORAGE_KEY);
539
682
  }, []);
540
683
  const value = useMemo(
@@ -567,6 +710,8 @@ function HypurrConnectProvider({
567
710
  agentReady,
568
711
  clearAgent: handleClearAgent,
569
712
  botId: config.telegram?.botId ?? "",
713
+ botUsername: config.telegram?.botUsername ?? "",
714
+ useWidget: config.telegram?.useWidget ?? false,
570
715
  authDataMap,
571
716
  telegramClient: tgClient,
572
717
  staticClient
@@ -601,6 +746,8 @@ function HypurrConnectProvider({
601
746
  agentReady,
602
747
  handleClearAgent,
603
748
  config.telegram?.botId,
749
+ config.telegram?.botUsername,
750
+ config.telegram?.useWidget,
604
751
  authDataMap,
605
752
  tgClient,
606
753
  staticClient
@@ -617,7 +764,7 @@ import {
617
764
  } from "framer-motion";
618
765
  import {
619
766
  useCallback as useCallback2,
620
- useEffect as useEffect2,
767
+ useEffect as useEffect3,
621
768
  useSyncExternalStore
622
769
  } from "react";
623
770
 
@@ -748,8 +895,53 @@ function TelegramColorIcon({ style }) {
748
895
  );
749
896
  }
750
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
+
751
943
  // src/LoginModal.tsx
752
- 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";
753
945
  var MOBILE_BREAKPOINT = 640;
754
946
  var btnStyle = {
755
947
  display: "flex",
@@ -827,7 +1019,7 @@ function HoverButton({
827
1019
  onClick,
828
1020
  children
829
1021
  }) {
830
- return /* @__PURE__ */ jsx4(
1022
+ return /* @__PURE__ */ jsx5(
831
1023
  motion.button,
832
1024
  {
833
1025
  type: "button",
@@ -839,7 +1031,14 @@ function HoverButton({
839
1031
  );
840
1032
  }
841
1033
  function LoginModal({ onConnectWallet, walletIcon }) {
842
- const { loginTelegram, loginModalOpen, closeLoginModal, botId } = useHypurrConnectInternal();
1034
+ const {
1035
+ loginTelegram,
1036
+ loginModalOpen,
1037
+ closeLoginModal,
1038
+ botId,
1039
+ botUsername,
1040
+ useWidget
1041
+ } = useHypurrConnectInternal();
843
1042
  const handleTelegramAuth = useCallback2(
844
1043
  (user) => {
845
1044
  loginTelegram(user);
@@ -847,7 +1046,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
847
1046
  },
848
1047
  [loginTelegram, closeLoginModal]
849
1048
  );
850
- useEffect2(() => {
1049
+ useEffect3(() => {
851
1050
  if (!loginModalOpen) return;
852
1051
  function onMessage(e) {
853
1052
  if (e.origin !== "https://oauth.telegram.org") return;
@@ -886,7 +1085,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
886
1085
  }, [botId]);
887
1086
  const isMobile = useIsMobile();
888
1087
  const modalContent = /* @__PURE__ */ jsxs3(Fragment, { children: [
889
- /* @__PURE__ */ jsx4(
1088
+ /* @__PURE__ */ jsx5(
890
1089
  "div",
891
1090
  {
892
1091
  style: {
@@ -897,13 +1096,19 @@ function LoginModal({ onConnectWallet, walletIcon }) {
897
1096
  gap: 8,
898
1097
  overflow: "hidden"
899
1098
  },
900
- children: /* @__PURE__ */ jsxs3(HoverButton, { onClick: openTelegramOAuth, children: [
901
- /* @__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 }),
902
1107
  "Telegram"
903
1108
  ] })
904
1109
  }
905
1110
  ),
906
- /* @__PURE__ */ jsx4("div", { style: dividerStyle }),
1111
+ /* @__PURE__ */ jsx5("div", { style: dividerStyle }),
907
1112
  /* @__PURE__ */ jsxs3(
908
1113
  HoverButton,
909
1114
  {
@@ -912,14 +1117,14 @@ function LoginModal({ onConnectWallet, walletIcon }) {
912
1117
  onConnectWallet();
913
1118
  },
914
1119
  children: [
915
- walletIcon ?? /* @__PURE__ */ jsx4(MetaMaskColorIcon, { style: iconSize }),
1120
+ walletIcon ?? /* @__PURE__ */ jsx5(MetaMaskColorIcon, { style: iconSize }),
916
1121
  "Wallet"
917
1122
  ]
918
1123
  }
919
1124
  )
920
1125
  ] });
921
- return /* @__PURE__ */ jsx4(AnimatePresence, { children: loginModalOpen && (isMobile ? /* @__PURE__ */ jsx4(MobileDrawer, { onClose: closeLoginModal, children: modalContent }, "drawer") : /* @__PURE__ */ jsxs3(Fragment, { children: [
922
- /* @__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(
923
1128
  motion.div,
924
1129
  {
925
1130
  style: backdropStyle,
@@ -931,7 +1136,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
931
1136
  },
932
1137
  "backdrop"
933
1138
  ),
934
- /* @__PURE__ */ jsx4(
1139
+ /* @__PURE__ */ jsx5(
935
1140
  motion.div,
936
1141
  {
937
1142
  style: modalWrapperStyle,
@@ -950,7 +1155,7 @@ function LoginModal({ onConnectWallet, walletIcon }) {
950
1155
  transition: { duration: 0.2, ease: "easeOut" },
951
1156
  onClick: (e) => e.stopPropagation(),
952
1157
  children: [
953
- /* @__PURE__ */ jsx4("p", { style: headingStyle, children: "Connect" }),
1158
+ /* @__PURE__ */ jsx5("p", { style: headingStyle, children: "Connect" }),
954
1159
  modalContent
955
1160
  ]
956
1161
  }
@@ -1017,7 +1222,7 @@ function MobileDrawer({
1017
1222
  [onClose, controls]
1018
1223
  );
1019
1224
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
1020
- /* @__PURE__ */ jsx4(
1225
+ /* @__PURE__ */ jsx5(
1021
1226
  motion.div,
1022
1227
  {
1023
1228
  style: backdropStyle,
@@ -1042,9 +1247,9 @@ function MobileDrawer({
1042
1247
  dragElastic: { top: 0, bottom: 0.4 },
1043
1248
  onDragEnd: handleDragEnd,
1044
1249
  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" }),
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" }),
1048
1253
  children
1049
1254
  ]
1050
1255
  },
@@ -1052,10 +1257,21 @@ function MobileDrawer({
1052
1257
  )
1053
1258
  ] });
1054
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
+ }
1055
1269
  export {
1056
1270
  GrpcExchangeTransport,
1057
1271
  HypurrConnectProvider,
1058
1272
  LoginModal,
1273
+ TelegramLoginWidget,
1274
+ createEoaSigner,
1059
1275
  createStaticClient,
1060
1276
  createTelegramClient,
1061
1277
  useHypurrConnect