@hfunlabs/hypurr-connect 0.1.10 → 0.1.12

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.
@@ -4,8 +4,8 @@ import {
4
4
  type IRequestTransport,
5
5
  } from "@hfunlabs/hyperliquid";
6
6
  import {
7
- PrivateKeySigner,
8
7
  signUserSignedAction,
8
+ type AbstractViemLocalAccount,
9
9
  } from "@hfunlabs/hyperliquid/signing";
10
10
  import type { RpcOptions } from "@protobuf-ts/runtime-rpc";
11
11
  import type { TelegramUserResponse } from "hypurr-grpc/ts/hypurr/telegram/telegram_service";
@@ -36,9 +36,12 @@ import {
36
36
  } from "./agent";
37
37
  import { createStaticClient, createTelegramClient } from "./grpc";
38
38
  import { GrpcExchangeTransport } from "./GrpcExchangeTransport";
39
+ import { PrivateKeySigner } from "./privateKeySigner";
39
40
  import type {
40
41
  AuthMethod,
41
42
  EoaSigner,
43
+ EvmTransactionRequest,
44
+ Hex,
42
45
  HypurrConnectConfig,
43
46
  HypurrConnectState,
44
47
  HypurrUser,
@@ -57,7 +60,10 @@ const TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-jwt";
57
60
  const LEGACY_TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-user";
58
61
  const TELEGRAM_AUTH_STATE_KEY = "hypurr-connect-auth-state";
59
62
  const TELEGRAM_AUTH_MESSAGE = "hypurr-connect:telegram-auth";
60
- const DEFAULT_AUTH_HUB_URL = "https://127.0.0.1:443/login";
63
+ const DEFAULT_AUTH_HUB_URL = "https://auth.hypurr.fun/login";
64
+ const DEFAULT_MEDIA_URL = "https://media.hypurr.fun";
65
+ const IGNORED_EXTERNAL_SIGNATURE =
66
+ "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b" as const;
61
67
  const DEFAULT_TELEGRAM_SCOPES = [
62
68
  "telegram:user:read",
63
69
  "telegram:wallet:read",
@@ -67,8 +73,20 @@ const DEFAULT_TELEGRAM_SCOPES = [
67
73
  "telegram:cabal:read",
68
74
  "telegram:cabal:write",
69
75
  "telegram:agent:write",
76
+ "telegram:support:read",
77
+ "telegram:support:write",
70
78
  ];
71
79
 
80
+ function createExternalSigningWallet(address: Hex): AbstractViemLocalAccount {
81
+ return {
82
+ address,
83
+ signTypedData(params) {
84
+ void params;
85
+ return Promise.resolve(IGNORED_EXTERNAL_SIGNATURE);
86
+ },
87
+ };
88
+ }
89
+
72
90
  function isInvalidTelegramAuthError(err: unknown): boolean {
73
91
  const msg =
74
92
  err instanceof Error
@@ -81,6 +99,64 @@ function isInvalidTelegramAuthError(err: unknown): boolean {
81
99
  );
82
100
  }
83
101
 
102
+ function normalizeMediaUrl(mediaUrl?: string): string {
103
+ return (mediaUrl?.trim() || DEFAULT_MEDIA_URL).replace(/\/+$/, "");
104
+ }
105
+
106
+ function isAddress(value?: string | null): value is Hex {
107
+ return !!value && /^0x[a-fA-F0-9]{40}$/.test(value);
108
+ }
109
+
110
+ function getRawSignedTransaction(result: unknown): Hex | null {
111
+ if (typeof result === "string" && result.startsWith("0x")) {
112
+ return result as Hex;
113
+ }
114
+ if (!result || typeof result !== "object") return null;
115
+
116
+ const record = result as Record<string, unknown>;
117
+ const candidates = [
118
+ record.raw,
119
+ record.rawTransaction,
120
+ record.signedTransaction,
121
+ record.serializedTransaction,
122
+ record.result,
123
+ ];
124
+ const raw = candidates.find(
125
+ (value): value is string =>
126
+ typeof value === "string" && value.startsWith("0x"),
127
+ );
128
+ return raw ? (raw as Hex) : null;
129
+ }
130
+
131
+ function decodeJsonBytes(bytes: Uint8Array): unknown {
132
+ const text = new TextDecoder().decode(bytes);
133
+ if (!text) return null;
134
+ try {
135
+ return JSON.parse(text);
136
+ } catch {
137
+ return text;
138
+ }
139
+ }
140
+
141
+ function encodeJsonBytes(value: unknown): Uint8Array {
142
+ return new TextEncoder().encode(JSON.stringify(value));
143
+ }
144
+
145
+ function withExpectedFrom(
146
+ transaction: EvmTransactionRequest,
147
+ address: Hex,
148
+ ): EvmTransactionRequest {
149
+ if (
150
+ transaction.from &&
151
+ transaction.from.toLowerCase() !== address.toLowerCase()
152
+ ) {
153
+ throw new Error(
154
+ "[HypurrConnect] EVM transaction `from` does not match the connected wallet.",
155
+ );
156
+ }
157
+ return transaction.from ? transaction : { ...transaction, from: address };
158
+ }
159
+
84
160
  function currentReturnTo(): string {
85
161
  const url = new URL(window.location.href);
86
162
  for (const param of [
@@ -108,9 +184,7 @@ function normalizeScopes(scope?: string | string[]): string {
108
184
  return scope?.trim() || DEFAULT_TELEGRAM_SCOPES.join(" ");
109
185
  }
110
186
 
111
- function isTelegramAuthMessage(
112
- data: unknown,
113
- ): data is {
187
+ function isTelegramAuthMessage(data: unknown): data is {
114
188
  type: typeof TELEGRAM_AUTH_MESSAGE;
115
189
  token: string;
116
190
  state: string;
@@ -423,12 +497,16 @@ export function HypurrConnectProvider({
423
497
 
424
498
  const user = useMemo<HypurrUser | null>(() => {
425
499
  if (tgAuthToken && authMethod === "telegram" && selectedWallet && tgUser) {
500
+ const mediaUrl = normalizeMediaUrl(config.mediaUrl);
426
501
  return {
427
502
  address: selectedWallet.ethereumAddress,
428
503
  walletId: selectedWallet.id,
429
504
  displayName: tgUser.telegramUsername
430
505
  ? `@${tgUser.telegramUsername}`
431
506
  : `Telegram ${tgUser.telegramId}`,
507
+ photoUrl: tgUser.pictureFileId
508
+ ? `${mediaUrl}/${tgUser.pictureFileId}`
509
+ : undefined,
432
510
  authMethod: "telegram",
433
511
  telegramId: String(tgUser.telegramId),
434
512
  hfunScore: tgUser?.reputation?.hfunScore,
@@ -444,10 +522,17 @@ export function HypurrConnectProvider({
444
522
  };
445
523
  }
446
524
  return null;
447
- }, [tgAuthToken, selectedWallet, eoaAddress, authMethod, tgUser]);
525
+ }, [
526
+ tgAuthToken,
527
+ selectedWallet,
528
+ eoaAddress,
529
+ authMethod,
530
+ tgUser,
531
+ config.mediaUrl,
532
+ ]);
448
533
 
449
534
  // ── Exchange client ──────────────────────────────────────────
450
- // Telegram: GrpcExchangeTransport → HyperliquidCoreAction (server signs)
535
+ // Telegram: dummy local wallet → GrpcExchangeTransport → HyperliquidCoreAction (server signs)
451
536
  // EOA: dual wallet — agent key for L1 actions, master signer for user-signed
452
537
  // actions (transfers, withdrawals, etc.). The dual wallet inspects the
453
538
  // EIP-712 domain name to decide which key signs each request.
@@ -493,8 +578,7 @@ export function HypurrConnectProvider({
493
578
  });
494
579
  return new ExchangeClient({
495
580
  transport,
496
- externalSigning: true,
497
- userAddress: user.address as `0x${string}`,
581
+ wallet: createExternalSigningWallet(user.address as Hex),
498
582
  });
499
583
  }
500
584
 
@@ -514,8 +598,7 @@ export function HypurrConnectProvider({
514
598
  };
515
599
  return new ExchangeClient({
516
600
  transport: noAgentTransport,
517
- externalSigning: true,
518
- userAddress: eoaAddress,
601
+ wallet: createExternalSigningWallet(eoaAddress),
519
602
  });
520
603
  }
521
604
 
@@ -655,20 +738,9 @@ export function HypurrConnectProvider({
655
738
  // Dual wallet: routes signing based on the EIP-712 domain.
656
739
  // "Exchange" domain → L1 action → agent key signs (auto-provisions if needed).
657
740
  // "HyperliquidSignTransaction" domain → user-signed → master wallet (popup).
658
- const dualWallet = {
741
+ const dualWallet: AbstractViemLocalAccount = {
659
742
  address: ownerAddress,
660
- async signTypedData(params: {
661
- domain: {
662
- name?: string;
663
- version?: string;
664
- chainId?: number;
665
- verifyingContract?: `0x${string}`;
666
- salt?: `0x${string}`;
667
- };
668
- types: Record<string, { name: string; type: string }[]>;
669
- primaryType: string;
670
- message: Record<string, unknown>;
671
- }): Promise<`0x${string}`> {
743
+ async signTypedData(params): Promise<`0x${string}`> {
672
744
  if (params.domain.name === "HyperliquidSignTransaction") {
673
745
  const signer = signerRef.current;
674
746
  if (!signer) {
@@ -715,6 +787,95 @@ export function HypurrConnectProvider({
715
787
  }
716
788
  }, [eoaAddress]);
717
789
 
790
+ const signEvmTransaction = useCallback(
791
+ async (transaction: EvmTransactionRequest): Promise<Hex> => {
792
+ if (authMethod === "eoa") {
793
+ if (!eoaAddress) {
794
+ throw new Error("[HypurrConnect] No EOA wallet connected.");
795
+ }
796
+
797
+ const signer = eoaSignerRef.current;
798
+ if (!signer) {
799
+ throw new Error(
800
+ "[HypurrConnect] No EOA signer available. Pass a signer to connectEoa(address, signer).",
801
+ );
802
+ }
803
+
804
+ const tx = withExpectedFrom(transaction, eoaAddress);
805
+ const result = signer.signTransaction
806
+ ? await signer.signTransaction(tx)
807
+ : signer.request
808
+ ? await signer.request({
809
+ method: "eth_signTransaction",
810
+ params: [tx],
811
+ })
812
+ : null;
813
+ const rawTransaction = getRawSignedTransaction(result);
814
+ if (!rawTransaction) {
815
+ throw new Error(
816
+ "[HypurrConnect] EOA signer did not return a raw transaction.",
817
+ );
818
+ }
819
+ return rawTransaction;
820
+ }
821
+
822
+ if (authMethod === "telegram") {
823
+ if (!telegramRpcOptions) {
824
+ throw new Error("[HypurrConnect] No Telegram RPC session available.");
825
+ }
826
+ if (!selectedWallet) {
827
+ throw new Error("[HypurrConnect] No Telegram wallet selected.");
828
+ }
829
+ if (
830
+ selectedWallet.id <= 0 ||
831
+ selectedWallet.isReadOnly ||
832
+ selectedWallet.isAgent
833
+ ) {
834
+ throw new Error(
835
+ "[HypurrConnect] Select a Telegram private-key wallet to sign EVM transactions.",
836
+ );
837
+ }
838
+ if (!isAddress(selectedWallet.ethereumAddress)) {
839
+ throw new Error(
840
+ "[HypurrConnect] Selected Telegram wallet does not have a valid EVM address.",
841
+ );
842
+ }
843
+
844
+ const tx = withExpectedFrom(
845
+ transaction,
846
+ selectedWallet.ethereumAddress,
847
+ );
848
+ try {
849
+ const signResponse = await tgClient.eVMSignTransaction(
850
+ {
851
+ authData: {},
852
+ walletId: selectedWallet.id,
853
+ params: encodeJsonBytes(tx),
854
+ },
855
+ telegramRpcOptions,
856
+ );
857
+ const rawTransaction = getRawSignedTransaction(
858
+ decodeJsonBytes(signResponse.response.result),
859
+ );
860
+ if (!rawTransaction) {
861
+ throw new Error(
862
+ "[HypurrConnect] Telegram signer did not return a raw transaction.",
863
+ );
864
+ }
865
+ return rawTransaction;
866
+ } catch (err) {
867
+ if (isInvalidTelegramAuthError(err)) {
868
+ onInvalidAuthRef.current?.();
869
+ }
870
+ throw err;
871
+ }
872
+ }
873
+
874
+ throw new Error("[HypurrConnect] No wallet connected.");
875
+ },
876
+ [authMethod, eoaAddress, selectedWallet, telegramRpcOptions, tgClient],
877
+ );
878
+
718
879
  // ── Wallet management (Telegram only) ───────────────────────
719
880
  const createWallet = useCallback(
720
881
  async (name: string): Promise<HyperliquidWallet> => {
@@ -1042,7 +1203,13 @@ export function HypurrConnectProvider({
1042
1203
  );
1043
1204
  }
1044
1205
 
1045
- eoaSignerRef.current = { signTypedData: signTypedDataAsync, chainId };
1206
+ const currentSigner = eoaSignerRef.current;
1207
+ eoaSignerRef.current = {
1208
+ signTypedData: signTypedDataAsync,
1209
+ signTransaction: currentSigner?.signTransaction,
1210
+ request: currentSigner?.request,
1211
+ chainId,
1212
+ };
1046
1213
 
1047
1214
  setEoaLoading(true);
1048
1215
  setEoaError(null);
@@ -1187,6 +1354,7 @@ export function HypurrConnectProvider({
1187
1354
 
1188
1355
  loginTelegram,
1189
1356
  connectEoa,
1357
+ signEvmTransaction,
1190
1358
  approveAgent: approveAgentFn,
1191
1359
  logout,
1192
1360
 
@@ -1233,6 +1401,7 @@ export function HypurrConnectProvider({
1233
1401
  closeLoginModal,
1234
1402
  loginTelegram,
1235
1403
  connectEoa,
1404
+ signEvmTransaction,
1236
1405
  approveAgentFn,
1237
1406
  logout,
1238
1407
  agent,
package/src/agent.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { StoredAgent } from "./types";
2
+ import { PrivateKeySigner } from "./privateKeySigner";
2
3
 
3
4
  export const AGENT_NAME = "hypurr-connect";
4
5
 
@@ -27,7 +28,7 @@ export function clearAgent(masterAddress: string): void {
27
28
 
28
29
  /**
29
30
  * Generate a random 32-byte private key and derive its address using the
30
- * SDK's PrivateKeySigner (no viem dependency needed).
31
+ * local PrivateKeySigner compatibility wrapper.
31
32
  */
32
33
  export async function generateAgentKey(): Promise<{
33
34
  privateKey: `0x${string}`;
@@ -39,7 +40,6 @@ export async function generateAgentKey(): Promise<{
39
40
  .join("");
40
41
  const privateKey = `0x${hex}` as `0x${string}`;
41
42
 
42
- const { PrivateKeySigner } = await import("@hfunlabs/hyperliquid/signing");
43
43
  const signer = new PrivateKeySigner(privateKey);
44
44
  return { privateKey, address: signer.address };
45
45
  }
package/src/index.ts CHANGED
@@ -7,14 +7,21 @@ export type { LoginModalProps } from "./LoginModal";
7
7
  export { GrpcExchangeTransport } from "./GrpcExchangeTransport";
8
8
  export type { GrpcExchangeTransportConfig } from "./GrpcExchangeTransport";
9
9
  export { createTelegramClient, createStaticClient } from "./grpc";
10
+ export { PrivateKeySigner } from "./privateKeySigner";
10
11
  export { createEoaSigner } from "./types";
11
12
  export type {
12
13
  AuthMethod,
14
+ EoaSignerOptions,
15
+ EoaSignTransactionFn,
13
16
  EoaSigner,
17
+ EvmRequestFn,
18
+ EvmTransactionRequest,
19
+ Hex,
14
20
  HypurrConnectConfig,
15
21
  HypurrConnectState,
16
22
  HypurrUser,
17
23
  ScaleCreateParams,
24
+ SignEvmTransactionFn,
18
25
  SignTypedDataFn,
19
26
  StoredAgent,
20
27
  TelegramLoginData,
@@ -0,0 +1,32 @@
1
+ import type { AbstractViemLocalAccount } from "@hfunlabs/hyperliquid/signing";
2
+ import { privateKeyToAccount, type PrivateKeyAccount } from "viem/accounts";
3
+ import type { Hex } from "./types";
4
+
5
+ type SignTypedDataParams = Parameters<
6
+ AbstractViemLocalAccount["signTypedData"]
7
+ >[0];
8
+
9
+ /**
10
+ * Compatibility wrapper for SDK versions that removed PrivateKeySigner.
11
+ *
12
+ * It exposes the viem local-account shape accepted by the Hyperliquid SDK.
13
+ */
14
+ export class PrivateKeySigner implements AbstractViemLocalAccount {
15
+ #account: PrivateKeyAccount;
16
+ readonly address: Hex;
17
+
18
+ constructor(privateKey: string) {
19
+ this.#account = privateKeyToAccount(normalizePrivateKey(privateKey));
20
+ this.address = this.#account.address;
21
+ }
22
+
23
+ signTypedData(params: SignTypedDataParams): Promise<Hex> {
24
+ return this.#account.signTypedData(
25
+ params as Parameters<PrivateKeyAccount["signTypedData"]>[0],
26
+ );
27
+ }
28
+ }
29
+
30
+ function normalizePrivateKey(privateKey: string): Hex {
31
+ return (privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`) as Hex;
32
+ }
package/src/types.ts CHANGED
@@ -19,6 +19,8 @@ import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
19
19
  export interface HypurrConnectConfig {
20
20
  /** gRPC-web base URL. Defaults to https://grpc.hypurr.fun. */
21
21
  grpcUrl?: string;
22
+ /** Media base URL for user-uploaded assets. Defaults to https://media.hypurr.fun. */
23
+ mediaUrl?: string;
22
24
  grpcTimeout?: number;
23
25
  isTestnet?: boolean;
24
26
  /** Polling interval in ms for TWAP/Scale session updates. Default 5000. Set 0 to disable. */
@@ -84,12 +86,52 @@ export type SignTypedDataFn = (params: {
84
86
  message: Record<string, unknown>;
85
87
  }) => Promise<`0x${string}`>;
86
88
 
89
+ export type Hex = `0x${string}`;
90
+
91
+ export interface EvmTransactionRequest {
92
+ from?: Hex;
93
+ to?: Hex;
94
+ gas?: Hex;
95
+ gasPrice?: Hex;
96
+ value?: Hex;
97
+ data?: Hex;
98
+ nonce?: Hex;
99
+ chainId?: Hex;
100
+ maxFeePerGas?: Hex;
101
+ maxPriorityFeePerGas?: Hex;
102
+ type?: Hex;
103
+ accessList?: unknown;
104
+ [key: string]: unknown;
105
+ }
106
+
107
+ export type EvmRequestFn = (args: {
108
+ method: string;
109
+ params?: unknown[];
110
+ }) => Promise<unknown>;
111
+
112
+ export type SignEvmTransactionFn = (
113
+ transaction: EvmTransactionRequest,
114
+ ) => Promise<Hex>;
115
+
116
+ export type EoaSignTransactionFn = (
117
+ transaction: EvmTransactionRequest,
118
+ ) => Promise<unknown>;
119
+
87
120
  /** Wallet signer provided at EOA connect time for user-signed actions. */
88
121
  export interface EoaSigner {
89
122
  signTypedData: SignTypedDataFn;
123
+ /** Optional raw EVM transaction signer. Used by `signEvmTransaction()` in EOA mode. */
124
+ signTransaction?: EoaSignTransactionFn;
125
+ /** Optional EIP-1193 provider request function. Used as a fallback for `eth_signTransaction`. */
126
+ request?: EvmRequestFn;
90
127
  chainId: number;
91
128
  }
92
129
 
130
+ export interface EoaSignerOptions {
131
+ signTransaction?: EoaSignTransactionFn;
132
+ request?: EvmRequestFn;
133
+ }
134
+
93
135
  /**
94
136
  * Create an {@link EoaSigner} from any EIP-712 signing function.
95
137
  *
@@ -118,6 +160,7 @@ export function createEoaSigner(
118
160
  | ((args: Record<string, unknown>) => Promise<`0x${string}`>)
119
161
  | { current: (args: Record<string, unknown>) => Promise<`0x${string}`> },
120
162
  chainId: number,
163
+ options: EoaSignerOptions = {},
121
164
  ): EoaSigner {
122
165
  const resolve =
123
166
  typeof signTypedDataAsync === "function"
@@ -125,6 +168,8 @@ export function createEoaSigner(
125
168
  : (args: Record<string, unknown>) => signTypedDataAsync.current(args);
126
169
  return {
127
170
  signTypedData: (params) => resolve(params),
171
+ signTransaction: options.signTransaction,
172
+ request: options.request,
128
173
  chainId,
129
174
  };
130
175
  }
@@ -214,6 +259,7 @@ export interface HypurrConnectState {
214
259
 
215
260
  // Auth actions
216
261
  connectEoa: (address: `0x${string}`, signer?: EoaSigner) => void;
262
+ signEvmTransaction: SignEvmTransactionFn;
217
263
  approveAgent: (
218
264
  signTypedDataAsync: SignTypedDataFn,
219
265
  chainId: number,