@hfunlabs/hypurr-connect 0.1.11 → 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,
@@ -59,6 +62,8 @@ const TELEGRAM_AUTH_STATE_KEY = "hypurr-connect-auth-state";
59
62
  const TELEGRAM_AUTH_MESSAGE = "hypurr-connect:telegram-auth";
60
63
  const DEFAULT_AUTH_HUB_URL = "https://auth.hypurr.fun/login";
61
64
  const DEFAULT_MEDIA_URL = "https://media.hypurr.fun";
65
+ const IGNORED_EXTERNAL_SIGNATURE =
66
+ "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b" as const;
62
67
  const DEFAULT_TELEGRAM_SCOPES = [
63
68
  "telegram:user:read",
64
69
  "telegram:wallet:read",
@@ -68,8 +73,20 @@ const DEFAULT_TELEGRAM_SCOPES = [
68
73
  "telegram:cabal:read",
69
74
  "telegram:cabal:write",
70
75
  "telegram:agent:write",
76
+ "telegram:support:read",
77
+ "telegram:support:write",
71
78
  ];
72
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
+
73
90
  function isInvalidTelegramAuthError(err: unknown): boolean {
74
91
  const msg =
75
92
  err instanceof Error
@@ -86,6 +103,60 @@ function normalizeMediaUrl(mediaUrl?: string): string {
86
103
  return (mediaUrl?.trim() || DEFAULT_MEDIA_URL).replace(/\/+$/, "");
87
104
  }
88
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
+
89
160
  function currentReturnTo(): string {
90
161
  const url = new URL(window.location.href);
91
162
  for (const param of [
@@ -113,9 +184,7 @@ function normalizeScopes(scope?: string | string[]): string {
113
184
  return scope?.trim() || DEFAULT_TELEGRAM_SCOPES.join(" ");
114
185
  }
115
186
 
116
- function isTelegramAuthMessage(
117
- data: unknown,
118
- ): data is {
187
+ function isTelegramAuthMessage(data: unknown): data is {
119
188
  type: typeof TELEGRAM_AUTH_MESSAGE;
120
189
  token: string;
121
190
  state: string;
@@ -463,7 +532,7 @@ export function HypurrConnectProvider({
463
532
  ]);
464
533
 
465
534
  // ── Exchange client ──────────────────────────────────────────
466
- // Telegram: GrpcExchangeTransport → HyperliquidCoreAction (server signs)
535
+ // Telegram: dummy local wallet → GrpcExchangeTransport → HyperliquidCoreAction (server signs)
467
536
  // EOA: dual wallet — agent key for L1 actions, master signer for user-signed
468
537
  // actions (transfers, withdrawals, etc.). The dual wallet inspects the
469
538
  // EIP-712 domain name to decide which key signs each request.
@@ -509,8 +578,7 @@ export function HypurrConnectProvider({
509
578
  });
510
579
  return new ExchangeClient({
511
580
  transport,
512
- externalSigning: true,
513
- userAddress: user.address as `0x${string}`,
581
+ wallet: createExternalSigningWallet(user.address as Hex),
514
582
  });
515
583
  }
516
584
 
@@ -530,8 +598,7 @@ export function HypurrConnectProvider({
530
598
  };
531
599
  return new ExchangeClient({
532
600
  transport: noAgentTransport,
533
- externalSigning: true,
534
- userAddress: eoaAddress,
601
+ wallet: createExternalSigningWallet(eoaAddress),
535
602
  });
536
603
  }
537
604
 
@@ -671,20 +738,9 @@ export function HypurrConnectProvider({
671
738
  // Dual wallet: routes signing based on the EIP-712 domain.
672
739
  // "Exchange" domain → L1 action → agent key signs (auto-provisions if needed).
673
740
  // "HyperliquidSignTransaction" domain → user-signed → master wallet (popup).
674
- const dualWallet = {
741
+ const dualWallet: AbstractViemLocalAccount = {
675
742
  address: ownerAddress,
676
- async signTypedData(params: {
677
- domain: {
678
- name?: string;
679
- version?: string;
680
- chainId?: number;
681
- verifyingContract?: `0x${string}`;
682
- salt?: `0x${string}`;
683
- };
684
- types: Record<string, { name: string; type: string }[]>;
685
- primaryType: string;
686
- message: Record<string, unknown>;
687
- }): Promise<`0x${string}`> {
743
+ async signTypedData(params): Promise<`0x${string}`> {
688
744
  if (params.domain.name === "HyperliquidSignTransaction") {
689
745
  const signer = signerRef.current;
690
746
  if (!signer) {
@@ -731,6 +787,95 @@ export function HypurrConnectProvider({
731
787
  }
732
788
  }, [eoaAddress]);
733
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
+
734
879
  // ── Wallet management (Telegram only) ───────────────────────
735
880
  const createWallet = useCallback(
736
881
  async (name: string): Promise<HyperliquidWallet> => {
@@ -1058,7 +1203,13 @@ export function HypurrConnectProvider({
1058
1203
  );
1059
1204
  }
1060
1205
 
1061
- 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
+ };
1062
1213
 
1063
1214
  setEoaLoading(true);
1064
1215
  setEoaError(null);
@@ -1203,6 +1354,7 @@ export function HypurrConnectProvider({
1203
1354
 
1204
1355
  loginTelegram,
1205
1356
  connectEoa,
1357
+ signEvmTransaction,
1206
1358
  approveAgent: approveAgentFn,
1207
1359
  logout,
1208
1360
 
@@ -1249,6 +1401,7 @@ export function HypurrConnectProvider({
1249
1401
  closeLoginModal,
1250
1402
  loginTelegram,
1251
1403
  connectEoa,
1404
+ signEvmTransaction,
1252
1405
  approveAgentFn,
1253
1406
  logout,
1254
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
@@ -86,12 +86,52 @@ export type SignTypedDataFn = (params: {
86
86
  message: Record<string, unknown>;
87
87
  }) => Promise<`0x${string}`>;
88
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
+
89
120
  /** Wallet signer provided at EOA connect time for user-signed actions. */
90
121
  export interface EoaSigner {
91
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;
92
127
  chainId: number;
93
128
  }
94
129
 
130
+ export interface EoaSignerOptions {
131
+ signTransaction?: EoaSignTransactionFn;
132
+ request?: EvmRequestFn;
133
+ }
134
+
95
135
  /**
96
136
  * Create an {@link EoaSigner} from any EIP-712 signing function.
97
137
  *
@@ -120,6 +160,7 @@ export function createEoaSigner(
120
160
  | ((args: Record<string, unknown>) => Promise<`0x${string}`>)
121
161
  | { current: (args: Record<string, unknown>) => Promise<`0x${string}`> },
122
162
  chainId: number,
163
+ options: EoaSignerOptions = {},
123
164
  ): EoaSigner {
124
165
  const resolve =
125
166
  typeof signTypedDataAsync === "function"
@@ -127,6 +168,8 @@ export function createEoaSigner(
127
168
  : (args: Record<string, unknown>) => signTypedDataAsync.current(args);
128
169
  return {
129
170
  signTypedData: (params) => resolve(params),
171
+ signTransaction: options.signTransaction,
172
+ request: options.request,
130
173
  chainId,
131
174
  };
132
175
  }
@@ -216,6 +259,7 @@ export interface HypurrConnectState {
216
259
 
217
260
  // Auth actions
218
261
  connectEoa: (address: `0x${string}`, signer?: EoaSigner) => void;
262
+ signEvmTransaction: SignEvmTransactionFn;
219
263
  approveAgent: (
220
264
  signTypedDataAsync: SignTypedDataFn,
221
265
  chainId: number,