@hfunlabs/hypurr-connect 0.1.1 → 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/README.md +294 -79
- package/dist/index.d.ts +77 -9
- package/dist/index.js +545 -164
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/HypurrConnectProvider.tsx +557 -164
- package/src/LoginModal.tsx +21 -7
- package/src/TelegramLoginWidget.tsx +62 -0
- package/src/agent.ts +80 -0
- package/src/index.ts +8 -0
- package/src/types.ts +89 -11
package/src/LoginModal.tsx
CHANGED
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
type CSSProperties,
|
|
12
12
|
type ReactNode,
|
|
13
13
|
} from "react";
|
|
14
|
-
import {
|
|
14
|
+
import { useHypurrConnectInternal } from "./HypurrConnectProvider";
|
|
15
15
|
import { MetaMaskColorIcon } from "./icons/MetaMaskColorIcon";
|
|
16
16
|
import { TelegramColorIcon } from "./icons/TelegramColorIcon";
|
|
17
|
+
import { TelegramLoginWidget } from "./TelegramLoginWidget";
|
|
17
18
|
import type { TelegramLoginData } from "./types";
|
|
18
19
|
|
|
19
20
|
export interface LoginModalProps {
|
|
@@ -130,8 +131,14 @@ function HoverButton({
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
|
|
133
|
-
const {
|
|
134
|
-
|
|
134
|
+
const {
|
|
135
|
+
loginTelegram,
|
|
136
|
+
loginModalOpen,
|
|
137
|
+
closeLoginModal,
|
|
138
|
+
botId,
|
|
139
|
+
botUsername,
|
|
140
|
+
useWidget,
|
|
141
|
+
} = useHypurrConnectInternal();
|
|
135
142
|
|
|
136
143
|
const handleTelegramAuth = useCallback(
|
|
137
144
|
(user: TelegramLoginData) => {
|
|
@@ -195,10 +202,17 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
|
|
|
195
202
|
overflow: "hidden",
|
|
196
203
|
}}
|
|
197
204
|
>
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
205
|
+
{useWidget && botUsername ? (
|
|
206
|
+
<TelegramLoginWidget
|
|
207
|
+
botUsername={botUsername}
|
|
208
|
+
onAuth={handleTelegramAuth}
|
|
209
|
+
/>
|
|
210
|
+
) : (
|
|
211
|
+
<HoverButton onClick={openTelegramOAuth}>
|
|
212
|
+
<TelegramColorIcon style={iconSize} />
|
|
213
|
+
Telegram
|
|
214
|
+
</HoverButton>
|
|
215
|
+
)}
|
|
202
216
|
</div>
|
|
203
217
|
|
|
204
218
|
<div style={dividerStyle} />
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import type { TelegramLoginData } from "./types";
|
|
3
|
+
|
|
4
|
+
const WIDGET_SCRIPT_URL = "https://telegram.org/js/telegram-widget.js?22";
|
|
5
|
+
const CALLBACK_NAME = "__hypurrConnectTelegramAuth";
|
|
6
|
+
|
|
7
|
+
export interface TelegramLoginWidgetProps {
|
|
8
|
+
botUsername: string;
|
|
9
|
+
onAuth: (data: TelegramLoginData) => void;
|
|
10
|
+
buttonSize?: "large" | "medium" | "small";
|
|
11
|
+
cornerRadius?: number;
|
|
12
|
+
showUserPhoto?: boolean;
|
|
13
|
+
requestAccess?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TelegramLoginWidget({
|
|
17
|
+
botUsername,
|
|
18
|
+
onAuth,
|
|
19
|
+
buttonSize = "large",
|
|
20
|
+
cornerRadius,
|
|
21
|
+
showUserPhoto = true,
|
|
22
|
+
requestAccess = true,
|
|
23
|
+
}: TelegramLoginWidgetProps) {
|
|
24
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
const onAuthRef = useRef(onAuth);
|
|
26
|
+
onAuthRef.current = onAuth;
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const container = containerRef.current;
|
|
30
|
+
if (!container) return;
|
|
31
|
+
|
|
32
|
+
(window as unknown as Record<string, unknown>)[CALLBACK_NAME] = (
|
|
33
|
+
user: TelegramLoginData,
|
|
34
|
+
) => {
|
|
35
|
+
onAuthRef.current(user);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const script = document.createElement("script");
|
|
39
|
+
script.src = WIDGET_SCRIPT_URL;
|
|
40
|
+
script.async = true;
|
|
41
|
+
script.setAttribute("data-telegram-login", botUsername);
|
|
42
|
+
script.setAttribute("data-size", buttonSize);
|
|
43
|
+
script.setAttribute("data-onauth", `${CALLBACK_NAME}(user)`);
|
|
44
|
+
script.setAttribute("data-userpic", String(showUserPhoto));
|
|
45
|
+
if (requestAccess) {
|
|
46
|
+
script.setAttribute("data-request-access", "write");
|
|
47
|
+
}
|
|
48
|
+
if (cornerRadius !== undefined) {
|
|
49
|
+
script.setAttribute("data-radius", String(cornerRadius));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
container.innerHTML = "";
|
|
53
|
+
container.appendChild(script);
|
|
54
|
+
|
|
55
|
+
return () => {
|
|
56
|
+
container.innerHTML = "";
|
|
57
|
+
delete (window as unknown as Record<string, unknown>)[CALLBACK_NAME];
|
|
58
|
+
};
|
|
59
|
+
}, [botUsername, buttonSize, cornerRadius, showUserPhoto, requestAccess]);
|
|
60
|
+
|
|
61
|
+
return <div ref={containerRef} />;
|
|
62
|
+
}
|
package/src/agent.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { StoredAgent } from "./types";
|
|
2
2
|
|
|
3
|
+
export const AGENT_NAME = "hypurr-connect";
|
|
4
|
+
|
|
3
5
|
const AGENT_STORAGE_PREFIX = "hypurr-connect-agent";
|
|
4
6
|
|
|
5
7
|
function storageKey(masterAddress: string): string {
|
|
@@ -41,3 +43,81 @@ export async function generateAgentKey(): Promise<{
|
|
|
41
43
|
const signer = new PrivateKeySigner(privateKey);
|
|
42
44
|
return { privateKey, address: signer.address };
|
|
43
45
|
}
|
|
46
|
+
|
|
47
|
+
interface ExtraAgent {
|
|
48
|
+
address: string;
|
|
49
|
+
name: string;
|
|
50
|
+
validUntil: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Query the Hyperliquid info API for the named agents registered to a user.
|
|
55
|
+
* Returns the matching entry for AGENT_NAME if it exists and is still valid.
|
|
56
|
+
*/
|
|
57
|
+
export async function fetchActiveAgent(
|
|
58
|
+
userAddress: string,
|
|
59
|
+
isTestnet: boolean,
|
|
60
|
+
): Promise<ExtraAgent | null> {
|
|
61
|
+
const url = isTestnet
|
|
62
|
+
? "https://api.hyperliquid-testnet.xyz/info"
|
|
63
|
+
: "https://api.hyperliquid.xyz/info";
|
|
64
|
+
|
|
65
|
+
const res = await fetch(url, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ type: "extraAgents", user: userAddress }),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!res.ok) return null;
|
|
72
|
+
|
|
73
|
+
const agents: unknown = await res.json();
|
|
74
|
+
if (!Array.isArray(agents)) return null;
|
|
75
|
+
|
|
76
|
+
const nowMs = Date.now();
|
|
77
|
+
const match = (agents as ExtraAgent[]).find(
|
|
78
|
+
(a) => a.name === AGENT_NAME && a.validUntil * 1000 > nowMs,
|
|
79
|
+
);
|
|
80
|
+
if (!match) return null;
|
|
81
|
+
return { ...match, validUntil: match.validUntil * 1000 };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Checks whether a stored agent is still valid: the address must appear in the
|
|
86
|
+
* on-chain `extraAgents` list and not be expired.
|
|
87
|
+
*/
|
|
88
|
+
export async function isAgentValid(
|
|
89
|
+
stored: StoredAgent,
|
|
90
|
+
userAddress: string,
|
|
91
|
+
isTestnet: boolean,
|
|
92
|
+
): Promise<boolean> {
|
|
93
|
+
if (stored.validUntil <= Date.now()) return false;
|
|
94
|
+
|
|
95
|
+
const remote = await fetchActiveAgent(userAddress, isTestnet);
|
|
96
|
+
if (!remote) return false;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
remote.address.toLowerCase() === stored.address.toLowerCase() &&
|
|
100
|
+
remote.validUntil > Date.now()
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const DEAD_AGENT_PATTERNS = [
|
|
105
|
+
/agent address .+ is not valid/i,
|
|
106
|
+
/unknown signer/i,
|
|
107
|
+
/not authorized/i,
|
|
108
|
+
/not an agent/i,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns true if the error indicates the agent has been pruned, expired,
|
|
113
|
+
* or is otherwise no longer registered on-chain.
|
|
114
|
+
*/
|
|
115
|
+
export function isDeadAgentError(err: unknown): boolean {
|
|
116
|
+
const msg =
|
|
117
|
+
err instanceof Error
|
|
118
|
+
? err.message
|
|
119
|
+
: typeof err === "object" && err !== null && "message" in err
|
|
120
|
+
? String((err as { message: unknown }).message)
|
|
121
|
+
: String(err);
|
|
122
|
+
return DEAD_AGENT_PATTERNS.some((p) => p.test(msg));
|
|
123
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,11 +4,15 @@ export {
|
|
|
4
4
|
} from "./HypurrConnectProvider";
|
|
5
5
|
export { LoginModal } from "./LoginModal";
|
|
6
6
|
export type { LoginModalProps } from "./LoginModal";
|
|
7
|
+
export { TelegramLoginWidget } from "./TelegramLoginWidget";
|
|
8
|
+
export type { TelegramLoginWidgetProps } from "./TelegramLoginWidget";
|
|
7
9
|
export { GrpcExchangeTransport } from "./GrpcExchangeTransport";
|
|
8
10
|
export type { GrpcExchangeTransportConfig } from "./GrpcExchangeTransport";
|
|
9
11
|
export { createTelegramClient, createStaticClient } from "./grpc";
|
|
12
|
+
export { createEoaSigner } from "./types";
|
|
10
13
|
export type {
|
|
11
14
|
AuthMethod,
|
|
15
|
+
EoaSigner,
|
|
12
16
|
HypurrConnectConfig,
|
|
13
17
|
HypurrConnectState,
|
|
14
18
|
HypurrUser,
|
|
@@ -16,3 +20,7 @@ export type {
|
|
|
16
20
|
StoredAgent,
|
|
17
21
|
TelegramLoginData,
|
|
18
22
|
} from "./types";
|
|
23
|
+
|
|
24
|
+
// Re-export wallet types from hypurr-grpc so consumers don't need the dependency
|
|
25
|
+
export type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
|
|
26
|
+
export type { TelegramChatWalletPack } from "hypurr-grpc/ts/hypurr/user";
|
package/src/types.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { ExchangeClient } from "@hfunlabs/hyperliquid";
|
|
2
2
|
import type { StaticClient } from "hypurr-grpc/ts/hypurr/static/static_service.client";
|
|
3
3
|
import type { TelegramClient } from "hypurr-grpc/ts/hypurr/telegram/telegram_service.client";
|
|
4
|
+
import type { TelegramChatWalletPack } from "hypurr-grpc/ts/hypurr/user";
|
|
5
|
+
import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
|
|
4
6
|
|
|
5
7
|
// ─── Config ──────────────────────────────────────────────────────
|
|
6
8
|
|
|
7
9
|
export interface HypurrConnectConfig {
|
|
8
10
|
grpcTimeout?: number;
|
|
9
11
|
isTestnet?: boolean;
|
|
10
|
-
telegram
|
|
12
|
+
telegram: {
|
|
11
13
|
botUsername: string;
|
|
12
|
-
botId
|
|
14
|
+
botId?: string;
|
|
15
|
+
useWidget: boolean;
|
|
13
16
|
};
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -36,6 +39,8 @@ export interface HypurrUser {
|
|
|
36
39
|
photoUrl?: string;
|
|
37
40
|
authMethod: AuthMethod;
|
|
38
41
|
telegramId?: string;
|
|
42
|
+
hfunScore?: number;
|
|
43
|
+
reputationScore?: number;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
// ─── Agent (EOA flow) ────────────────────────────────────────────
|
|
@@ -44,6 +49,8 @@ export interface StoredAgent {
|
|
|
44
49
|
privateKey: `0x${string}`;
|
|
45
50
|
address: `0x${string}`;
|
|
46
51
|
approvedAt: number;
|
|
52
|
+
/** Epoch ms from the `extraAgents` response; agent is invalid after this time. */
|
|
53
|
+
validUntil: number;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
export type SignTypedDataFn = (params: {
|
|
@@ -53,6 +60,51 @@ export type SignTypedDataFn = (params: {
|
|
|
53
60
|
message: Record<string, unknown>;
|
|
54
61
|
}) => Promise<`0x${string}`>;
|
|
55
62
|
|
|
63
|
+
/** Wallet signer provided at EOA connect time for user-signed actions. */
|
|
64
|
+
export interface EoaSigner {
|
|
65
|
+
signTypedData: SignTypedDataFn;
|
|
66
|
+
chainId: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create an {@link EoaSigner} from any EIP-712 signing function.
|
|
71
|
+
*
|
|
72
|
+
* Accepts either a direct function or a `{ current }` ref object so the
|
|
73
|
+
* signer always calls through to the latest function (avoids stale closures
|
|
74
|
+
* with React hooks like wagmi's `useSignTypedData`).
|
|
75
|
+
*
|
|
76
|
+
* @example wagmi v2 — ref pattern (recommended)
|
|
77
|
+
* ```ts
|
|
78
|
+
* const { signTypedDataAsync } = useSignTypedData();
|
|
79
|
+
* const chainId = useChainId();
|
|
80
|
+
* const signerRef = useRef(signTypedDataAsync);
|
|
81
|
+
* signerRef.current = signTypedDataAsync; // stays fresh every render
|
|
82
|
+
*
|
|
83
|
+
* // call once — the ref keeps it up to date
|
|
84
|
+
* connectEoa(address, createEoaSigner(signerRef, chainId));
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @example direct function (e.g. from viem WalletClient)
|
|
88
|
+
* ```ts
|
|
89
|
+
* connectEoa(address, createEoaSigner(client.signTypedData, chainId));
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function createEoaSigner(
|
|
93
|
+
signTypedDataAsync:
|
|
94
|
+
| ((args: Record<string, unknown>) => Promise<`0x${string}`>)
|
|
95
|
+
| { current: (args: Record<string, unknown>) => Promise<`0x${string}`> },
|
|
96
|
+
chainId: number,
|
|
97
|
+
): EoaSigner {
|
|
98
|
+
const resolve =
|
|
99
|
+
typeof signTypedDataAsync === "function"
|
|
100
|
+
? signTypedDataAsync
|
|
101
|
+
: (args: Record<string, unknown>) => signTypedDataAsync.current(args);
|
|
102
|
+
return {
|
|
103
|
+
signTypedData: (params) => resolve(params),
|
|
104
|
+
chainId,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
56
108
|
// ─── Context state ───────────────────────────────────────────────
|
|
57
109
|
|
|
58
110
|
export interface HypurrConnectState {
|
|
@@ -63,16 +115,40 @@ export interface HypurrConnectState {
|
|
|
63
115
|
error: string | null;
|
|
64
116
|
authMethod: AuthMethod;
|
|
65
117
|
|
|
66
|
-
// SDK ExchangeClient —
|
|
118
|
+
// SDK ExchangeClient — handles both L1 (agent-signed) and user-signed actions.
|
|
67
119
|
// Telegram: backed by GrpcExchangeTransport (HyperliquidCoreAction)
|
|
68
|
-
// EOA:
|
|
120
|
+
// EOA: uses a dual wallet — agent key for L1 actions, master wallet for
|
|
121
|
+
// user-signed actions (transfers, withdrawals, etc.) when a signer is provided.
|
|
69
122
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
123
|
exchange: ExchangeClient<any> | null;
|
|
71
124
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
125
|
+
// Multi-wallet (Telegram only — EOA has a single wallet)
|
|
126
|
+
wallets: HyperliquidWallet[];
|
|
127
|
+
selectedWalletId: number;
|
|
128
|
+
selectWallet: (walletId: number) => void;
|
|
129
|
+
|
|
130
|
+
// Wallet management (Telegram only)
|
|
131
|
+
createWallet: (name: string) => Promise<HyperliquidWallet>;
|
|
132
|
+
deleteWallet: (walletId: number) => Promise<void>;
|
|
133
|
+
refreshWallets: () => void;
|
|
134
|
+
|
|
135
|
+
// Wallet packs & labels (Telegram only)
|
|
136
|
+
packs: TelegramChatWalletPack[];
|
|
137
|
+
createWalletPack: (name: string) => Promise<number>;
|
|
138
|
+
addPackLabel: (params: {
|
|
139
|
+
walletAddress: string;
|
|
140
|
+
walletLabel: string;
|
|
141
|
+
packId: number;
|
|
142
|
+
}) => Promise<void>;
|
|
143
|
+
modifyPackLabel: (params: {
|
|
144
|
+
walletLabelOld: string;
|
|
145
|
+
walletLabelNew: string;
|
|
146
|
+
packId: number;
|
|
147
|
+
}) => Promise<void>;
|
|
148
|
+
removePackLabel: (params: {
|
|
149
|
+
walletLabel: string;
|
|
150
|
+
packId: number;
|
|
151
|
+
}) => Promise<void>;
|
|
76
152
|
|
|
77
153
|
// Login modal
|
|
78
154
|
loginModalOpen: boolean;
|
|
@@ -80,14 +156,16 @@ export interface HypurrConnectState {
|
|
|
80
156
|
closeLoginModal: () => void;
|
|
81
157
|
|
|
82
158
|
// Auth actions
|
|
83
|
-
|
|
84
|
-
|
|
159
|
+
connectEoa: (address: `0x${string}`, signer?: EoaSigner) => void;
|
|
160
|
+
approveAgent: (
|
|
161
|
+
signTypedDataAsync: SignTypedDataFn,
|
|
162
|
+
chainId: number,
|
|
163
|
+
) => Promise<void>;
|
|
85
164
|
logout: () => void;
|
|
86
165
|
|
|
87
166
|
// EOA agent management
|
|
88
167
|
agent: StoredAgent | null;
|
|
89
168
|
agentReady: boolean;
|
|
90
|
-
approveAgent: (signTypedDataAsync: SignTypedDataFn) => Promise<void>;
|
|
91
169
|
clearAgent: () => void;
|
|
92
170
|
|
|
93
171
|
// Telegram config
|