@hfunlabs/hypurr-connect 0.1.0
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 +363 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +767 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
- package/src/GrpcExchangeTransport.ts +122 -0
- package/src/HypurrConnectProvider.tsx +418 -0
- package/src/LoginModal.tsx +230 -0
- package/src/agent.ts +43 -0
- package/src/grpc.ts +23 -0
- package/src/icons/MetaMaskColorIcon.tsx +53 -0
- package/src/icons/TelegramColorIcon.tsx +36 -0
- package/src/index.ts +18 -0
- package/src/types.ts +100 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { ExchangeClient, HttpTransport, InfoClient } from "@hfunlabs/hyperliquid";
|
|
2
|
+
import { PrivateKeySigner } from "@hfunlabs/hyperliquid/signing";
|
|
3
|
+
import type { TelegramUserResponse } from "hypurr-grpc/ts/hypurr/telegram/telegram_service";
|
|
4
|
+
import type { TelegramUser as HypurrTelegramUser } from "hypurr-grpc/ts/hypurr/user";
|
|
5
|
+
import {
|
|
6
|
+
createContext,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
useState,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
} from "react";
|
|
14
|
+
import {
|
|
15
|
+
clearAgent as clearStoredAgent,
|
|
16
|
+
generateAgentKey,
|
|
17
|
+
loadAgent,
|
|
18
|
+
saveAgent,
|
|
19
|
+
} from "./agent";
|
|
20
|
+
import { createStaticClient, createTelegramClient } from "./grpc";
|
|
21
|
+
import { GrpcExchangeTransport } from "./GrpcExchangeTransport";
|
|
22
|
+
import type {
|
|
23
|
+
AuthMethod,
|
|
24
|
+
HypurrConnectConfig,
|
|
25
|
+
HypurrConnectState,
|
|
26
|
+
HypurrUser,
|
|
27
|
+
SignTypedDataFn,
|
|
28
|
+
StoredAgent,
|
|
29
|
+
TelegramLoginData,
|
|
30
|
+
} from "./types";
|
|
31
|
+
|
|
32
|
+
const TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-user";
|
|
33
|
+
|
|
34
|
+
function toAuthDataMap(data: TelegramLoginData): Record<string, string> {
|
|
35
|
+
const map: Record<string, string> = {
|
|
36
|
+
id: String(data.id),
|
|
37
|
+
first_name: data.first_name,
|
|
38
|
+
auth_date: String(data.auth_date),
|
|
39
|
+
hash: data.hash,
|
|
40
|
+
};
|
|
41
|
+
if (data.last_name) map.last_name = data.last_name;
|
|
42
|
+
if (data.username) map.username = data.username;
|
|
43
|
+
if (data.photo_url) map.photo_url = data.photo_url;
|
|
44
|
+
return map;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const HypurrConnectContext = createContext<HypurrConnectState | null>(null);
|
|
48
|
+
|
|
49
|
+
export function useHypurrConnect(): HypurrConnectState {
|
|
50
|
+
const ctx = useContext(HypurrConnectContext);
|
|
51
|
+
if (!ctx)
|
|
52
|
+
throw new Error(
|
|
53
|
+
"useHypurrConnect must be used within <HypurrConnectProvider>",
|
|
54
|
+
);
|
|
55
|
+
return ctx;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function HypurrConnectProvider({
|
|
59
|
+
config,
|
|
60
|
+
children,
|
|
61
|
+
}: {
|
|
62
|
+
config: HypurrConnectConfig;
|
|
63
|
+
children: ReactNode;
|
|
64
|
+
}) {
|
|
65
|
+
const tgClient = useMemo(() => createTelegramClient(config), [config]);
|
|
66
|
+
const staticClient = useMemo(() => createStaticClient(config), [config]);
|
|
67
|
+
|
|
68
|
+
// ── Telegram auth state ──────────────────────────────────────
|
|
69
|
+
const [tgLoginData, setTgLoginData] = useState<TelegramLoginData | null>(
|
|
70
|
+
() => {
|
|
71
|
+
try {
|
|
72
|
+
const stored = localStorage.getItem(TELEGRAM_STORAGE_KEY);
|
|
73
|
+
return stored ? JSON.parse(stored) : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
const [tgUser, setTgUser] = useState<HypurrTelegramUser | null>(null);
|
|
80
|
+
const [tgLoading, setTgLoading] = useState(false);
|
|
81
|
+
const [tgError, setTgError] = useState<string | null>(null);
|
|
82
|
+
|
|
83
|
+
const authDataMap = useMemo(
|
|
84
|
+
() => (tgLoginData ? toAuthDataMap(tgLoginData) : {}),
|
|
85
|
+
[tgLoginData],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!tgLoginData) return;
|
|
90
|
+
let cancelled = false;
|
|
91
|
+
setTgLoading(true);
|
|
92
|
+
setTgError(null);
|
|
93
|
+
|
|
94
|
+
(async () => {
|
|
95
|
+
try {
|
|
96
|
+
const authData = toAuthDataMap(tgLoginData);
|
|
97
|
+
console.log(authData);
|
|
98
|
+
const { response } = await tgClient.telegramUser({ authData });
|
|
99
|
+
console.log(response);
|
|
100
|
+
if (cancelled) return;
|
|
101
|
+
setTgUser((response as TelegramUserResponse).user ?? null);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (cancelled) return;
|
|
104
|
+
console.error("[HypurrConnect] gRPC TelegramUser failed:", err);
|
|
105
|
+
setTgError(err instanceof Error ? err.message : String(err));
|
|
106
|
+
} finally {
|
|
107
|
+
if (!cancelled) setTgLoading(false);
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
cancelled = true;
|
|
113
|
+
};
|
|
114
|
+
}, [tgLoginData, tgClient]);
|
|
115
|
+
|
|
116
|
+
// ── EOA auth state ───────────────────────────────────────────
|
|
117
|
+
const [eoaAddress, setEoaAddress] = useState<`0x${string}` | null>(null);
|
|
118
|
+
const [agent, setAgent] = useState<StoredAgent | null>(null);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (eoaAddress) {
|
|
122
|
+
setAgent(loadAgent(eoaAddress));
|
|
123
|
+
} else {
|
|
124
|
+
setAgent(null);
|
|
125
|
+
}
|
|
126
|
+
}, [eoaAddress]);
|
|
127
|
+
|
|
128
|
+
// ── Derived auth ─────────────────────────────────────────────
|
|
129
|
+
const authMethod: AuthMethod = tgLoginData
|
|
130
|
+
? "telegram"
|
|
131
|
+
: eoaAddress
|
|
132
|
+
? "eoa"
|
|
133
|
+
: null;
|
|
134
|
+
|
|
135
|
+
const tgWallet = tgUser?.wallet ?? (tgUser?.wallets ?? [])[0] ?? null;
|
|
136
|
+
|
|
137
|
+
const user = useMemo<HypurrUser | null>(() => {
|
|
138
|
+
if (tgLoginData && authMethod === "telegram") {
|
|
139
|
+
return {
|
|
140
|
+
address: tgWallet?.ethereumAddress ?? "",
|
|
141
|
+
walletId: tgUser?.walletId ?? tgWallet?.id ?? 0,
|
|
142
|
+
displayName: tgLoginData.username
|
|
143
|
+
? `@${tgLoginData.username}`
|
|
144
|
+
: tgLoginData.first_name,
|
|
145
|
+
photoUrl: tgLoginData.photo_url,
|
|
146
|
+
authMethod: "telegram",
|
|
147
|
+
telegramId: String(tgLoginData.id),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (eoaAddress && authMethod === "eoa") {
|
|
151
|
+
return {
|
|
152
|
+
address: eoaAddress,
|
|
153
|
+
walletId: 0,
|
|
154
|
+
displayName: `${eoaAddress.slice(0, 6)}...${eoaAddress.slice(-4)}`,
|
|
155
|
+
authMethod: "eoa",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}, [tgLoginData, tgUser, tgWallet, eoaAddress, authMethod]);
|
|
160
|
+
|
|
161
|
+
// ── Exchange client ──────────────────────────────────────────
|
|
162
|
+
// Telegram: GrpcExchangeTransport → HyperliquidCoreAction (server signs)
|
|
163
|
+
// EOA: HttpTransport + agent wallet (SDK signs locally)
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
+
const exchange = useMemo<ExchangeClient<any> | null>(() => {
|
|
166
|
+
if (authMethod === "telegram" && user?.address) {
|
|
167
|
+
const transport = new GrpcExchangeTransport({
|
|
168
|
+
isTestnet: config.isTestnet ?? false,
|
|
169
|
+
telegramClient: tgClient,
|
|
170
|
+
authDataMap,
|
|
171
|
+
walletId: user.walletId,
|
|
172
|
+
});
|
|
173
|
+
return new ExchangeClient({
|
|
174
|
+
transport,
|
|
175
|
+
externalSigning: true,
|
|
176
|
+
userAddress: user.address as `0x${string}`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (authMethod === "eoa" && agent) {
|
|
181
|
+
const wallet = new PrivateKeySigner(agent.privateKey);
|
|
182
|
+
return new ExchangeClient({
|
|
183
|
+
transport: new HttpTransport({
|
|
184
|
+
isTestnet: config.isTestnet ?? false,
|
|
185
|
+
}),
|
|
186
|
+
wallet,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}, [authMethod, user, agent, config.isTestnet, tgClient, authDataMap]);
|
|
192
|
+
|
|
193
|
+
// ── USDC balance from Hyperliquid ──────────────────────────────
|
|
194
|
+
const infoClient = useMemo(
|
|
195
|
+
() =>
|
|
196
|
+
new InfoClient({
|
|
197
|
+
transport: new HttpTransport({
|
|
198
|
+
isTestnet: config.isTestnet ?? false,
|
|
199
|
+
}),
|
|
200
|
+
}),
|
|
201
|
+
[config.isTestnet],
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const [usdcBalance, setUsdcBalance] = useState<string | null>(null);
|
|
205
|
+
const [usdcBalanceLoading, setUsdcBalanceLoading] = useState(false);
|
|
206
|
+
const [balanceTick, setBalanceTick] = useState(0);
|
|
207
|
+
|
|
208
|
+
const refreshBalance = useCallback(() => setBalanceTick((t) => t + 1), []);
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
const addr = user?.address;
|
|
212
|
+
if (!addr) {
|
|
213
|
+
setUsdcBalance(null);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let cancelled = false;
|
|
218
|
+
setUsdcBalanceLoading(true);
|
|
219
|
+
|
|
220
|
+
(async () => {
|
|
221
|
+
try {
|
|
222
|
+
const state = await infoClient.clearinghouseState({
|
|
223
|
+
user: addr as `0x${string}`,
|
|
224
|
+
});
|
|
225
|
+
if (!cancelled) {
|
|
226
|
+
setUsdcBalance(state.withdrawable);
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error("[HypurrConnect] Failed to fetch USDC balance:", err);
|
|
230
|
+
if (!cancelled) setUsdcBalance(null);
|
|
231
|
+
} finally {
|
|
232
|
+
if (!cancelled) setUsdcBalanceLoading(false);
|
|
233
|
+
}
|
|
234
|
+
})();
|
|
235
|
+
|
|
236
|
+
return () => {
|
|
237
|
+
cancelled = true;
|
|
238
|
+
};
|
|
239
|
+
}, [user?.address, infoClient, balanceTick]);
|
|
240
|
+
|
|
241
|
+
// ── Agent approval (EOA flow) ────────────────────────────────
|
|
242
|
+
const approveAgent = useCallback(
|
|
243
|
+
async (signTypedDataAsync: SignTypedDataFn) => {
|
|
244
|
+
if (!eoaAddress) throw new Error("No EOA address connected");
|
|
245
|
+
|
|
246
|
+
const { privateKey, address: agentAddress } = await generateAgentKey();
|
|
247
|
+
|
|
248
|
+
const isTestnet = config.isTestnet ?? false;
|
|
249
|
+
const nonce = Date.now();
|
|
250
|
+
const action = {
|
|
251
|
+
type: "approveAgent",
|
|
252
|
+
signatureChainId: isTestnet ? "0x66eee" : "0xa4b1",
|
|
253
|
+
hyperliquidChain: isTestnet ? "Testnet" : "Mainnet",
|
|
254
|
+
agentAddress: agentAddress.toLowerCase() as `0x${string}`,
|
|
255
|
+
agentName: null as string | null,
|
|
256
|
+
nonce,
|
|
257
|
+
};
|
|
258
|
+
const types = {
|
|
259
|
+
"HyperliquidTransaction:ApproveAgent": [
|
|
260
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
261
|
+
{ name: "agentAddress", type: "address" },
|
|
262
|
+
{ name: "agentName", type: "string" },
|
|
263
|
+
{ name: "nonce", type: "uint64" },
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const signature = await signTypedDataAsync({
|
|
268
|
+
domain: {
|
|
269
|
+
name: "HyperliquidSignTransaction",
|
|
270
|
+
version: "1",
|
|
271
|
+
chainId: isTestnet ? 421614 : 42161,
|
|
272
|
+
verifyingContract: "0x0000000000000000000000000000000000000000",
|
|
273
|
+
},
|
|
274
|
+
types,
|
|
275
|
+
primaryType: "HyperliquidTransaction:ApproveAgent",
|
|
276
|
+
message: {
|
|
277
|
+
hyperliquidChain: action.hyperliquidChain,
|
|
278
|
+
agentAddress: action.agentAddress,
|
|
279
|
+
agentName: "",
|
|
280
|
+
nonce: BigInt(nonce),
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const r = `0x${signature.slice(2, 66)}`;
|
|
285
|
+
const s = `0x${signature.slice(66, 130)}`;
|
|
286
|
+
const v = parseInt(signature.slice(130, 132), 16);
|
|
287
|
+
|
|
288
|
+
const url = isTestnet
|
|
289
|
+
? "https://api.hyperliquid-testnet.xyz/exchange"
|
|
290
|
+
: "https://api.hyperliquid.xyz/exchange";
|
|
291
|
+
|
|
292
|
+
const res = await fetch(url, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: { "Content-Type": "application/json" },
|
|
295
|
+
body: JSON.stringify({
|
|
296
|
+
action,
|
|
297
|
+
nonce,
|
|
298
|
+
signature: { r, s, v },
|
|
299
|
+
}),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const body = await res.json();
|
|
303
|
+
if (body?.status !== "ok") {
|
|
304
|
+
throw new Error(`approveAgent failed: ${JSON.stringify(body)}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const stored: StoredAgent = {
|
|
308
|
+
privateKey,
|
|
309
|
+
address: agentAddress,
|
|
310
|
+
approvedAt: Date.now(),
|
|
311
|
+
};
|
|
312
|
+
saveAgent(eoaAddress, stored);
|
|
313
|
+
setAgent(stored);
|
|
314
|
+
},
|
|
315
|
+
[eoaAddress, config.isTestnet],
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const handleClearAgent = useCallback(() => {
|
|
319
|
+
if (eoaAddress) {
|
|
320
|
+
clearStoredAgent(eoaAddress);
|
|
321
|
+
setAgent(null);
|
|
322
|
+
}
|
|
323
|
+
}, [eoaAddress]);
|
|
324
|
+
|
|
325
|
+
// ── Login modal state ────────────────────────────────────────
|
|
326
|
+
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
|
327
|
+
const openLoginModal = useCallback(() => setLoginModalOpen(true), []);
|
|
328
|
+
const closeLoginModal = useCallback(() => setLoginModalOpen(false), []);
|
|
329
|
+
|
|
330
|
+
// ── Auth actions ─────────────────────────────────────────────
|
|
331
|
+
const loginTelegram = useCallback((data: TelegramLoginData) => {
|
|
332
|
+
setTgLoginData(data);
|
|
333
|
+
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(data));
|
|
334
|
+
setEoaAddress(null);
|
|
335
|
+
setAgent(null);
|
|
336
|
+
}, []);
|
|
337
|
+
|
|
338
|
+
const loginEoa = useCallback((address: `0x${string}`) => {
|
|
339
|
+
setEoaAddress(address);
|
|
340
|
+
setTgLoginData(null);
|
|
341
|
+
setTgUser(null);
|
|
342
|
+
setTgError(null);
|
|
343
|
+
localStorage.removeItem(TELEGRAM_STORAGE_KEY);
|
|
344
|
+
}, []);
|
|
345
|
+
|
|
346
|
+
const logout = useCallback(() => {
|
|
347
|
+
setTgLoginData(null);
|
|
348
|
+
setTgUser(null);
|
|
349
|
+
setTgError(null);
|
|
350
|
+
setEoaAddress(null);
|
|
351
|
+
setAgent(null);
|
|
352
|
+
localStorage.removeItem(TELEGRAM_STORAGE_KEY);
|
|
353
|
+
}, []);
|
|
354
|
+
|
|
355
|
+
// ── Context value ────────────────────────────────────────────
|
|
356
|
+
const value = useMemo<HypurrConnectState>(
|
|
357
|
+
() => ({
|
|
358
|
+
user,
|
|
359
|
+
isLoggedIn: !!user,
|
|
360
|
+
isLoading: tgLoading,
|
|
361
|
+
error: tgError,
|
|
362
|
+
authMethod,
|
|
363
|
+
exchange,
|
|
364
|
+
|
|
365
|
+
usdcBalance,
|
|
366
|
+
usdcBalanceLoading,
|
|
367
|
+
refreshBalance,
|
|
368
|
+
|
|
369
|
+
loginModalOpen,
|
|
370
|
+
openLoginModal,
|
|
371
|
+
closeLoginModal,
|
|
372
|
+
|
|
373
|
+
loginTelegram,
|
|
374
|
+
loginEoa,
|
|
375
|
+
logout,
|
|
376
|
+
|
|
377
|
+
agent,
|
|
378
|
+
agentReady: authMethod === "telegram" || !!agent,
|
|
379
|
+
approveAgent,
|
|
380
|
+
clearAgent: handleClearAgent,
|
|
381
|
+
|
|
382
|
+
botId: config.telegram?.botId ?? "",
|
|
383
|
+
|
|
384
|
+
authDataMap,
|
|
385
|
+
telegramClient: tgClient,
|
|
386
|
+
staticClient,
|
|
387
|
+
}),
|
|
388
|
+
[
|
|
389
|
+
user,
|
|
390
|
+
tgLoading,
|
|
391
|
+
tgError,
|
|
392
|
+
authMethod,
|
|
393
|
+
exchange,
|
|
394
|
+
usdcBalance,
|
|
395
|
+
usdcBalanceLoading,
|
|
396
|
+
refreshBalance,
|
|
397
|
+
loginModalOpen,
|
|
398
|
+
openLoginModal,
|
|
399
|
+
closeLoginModal,
|
|
400
|
+
loginTelegram,
|
|
401
|
+
loginEoa,
|
|
402
|
+
logout,
|
|
403
|
+
agent,
|
|
404
|
+
approveAgent,
|
|
405
|
+
handleClearAgent,
|
|
406
|
+
config.telegram?.botId,
|
|
407
|
+
authDataMap,
|
|
408
|
+
tgClient,
|
|
409
|
+
staticClient,
|
|
410
|
+
],
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<HypurrConnectContext.Provider value={value}>
|
|
415
|
+
{children}
|
|
416
|
+
</HypurrConnectContext.Provider>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnimatePresence,
|
|
3
|
+
motion,
|
|
4
|
+
useAnimationControls,
|
|
5
|
+
type PanInfo,
|
|
6
|
+
} from "framer-motion";
|
|
7
|
+
import {
|
|
8
|
+
useCallback,
|
|
9
|
+
useEffect,
|
|
10
|
+
useSyncExternalStore,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
13
|
+
import { useHypurrConnect } from "./HypurrConnectProvider";
|
|
14
|
+
import { MetaMaskColorIcon } from "./icons/MetaMaskColorIcon";
|
|
15
|
+
import { TelegramColorIcon } from "./icons/TelegramColorIcon";
|
|
16
|
+
import type { TelegramLoginData } from "./types";
|
|
17
|
+
|
|
18
|
+
export interface LoginModalProps {
|
|
19
|
+
onConnectWallet: () => void;
|
|
20
|
+
walletIcon?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const MOBILE_BREAKPOINT = 640;
|
|
24
|
+
|
|
25
|
+
const btnClass =
|
|
26
|
+
"flex h-[53px] w-full items-center gap-3 overflow-hidden rounded bg-white/5 px-6 text-sm font-semibold tracking-tight text-white cursor-pointer transition-colors duration-150 hover:bg-white/10";
|
|
27
|
+
|
|
28
|
+
const mobileQuery =
|
|
29
|
+
typeof window !== "undefined"
|
|
30
|
+
? window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`)
|
|
31
|
+
: null;
|
|
32
|
+
|
|
33
|
+
function subscribeMobile(cb: () => void) {
|
|
34
|
+
mobileQuery?.addEventListener("change", cb);
|
|
35
|
+
return () => mobileQuery?.removeEventListener("change", cb);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getSnapshotMobile() {
|
|
39
|
+
return mobileQuery?.matches ?? false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function useIsMobile() {
|
|
43
|
+
return useSyncExternalStore(subscribeMobile, getSnapshotMobile, () => false);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
|
|
47
|
+
const { loginTelegram, loginModalOpen, closeLoginModal, botId } =
|
|
48
|
+
useHypurrConnect();
|
|
49
|
+
|
|
50
|
+
const handleTelegramAuth = useCallback(
|
|
51
|
+
(user: TelegramLoginData) => {
|
|
52
|
+
loginTelegram(user);
|
|
53
|
+
closeLoginModal();
|
|
54
|
+
},
|
|
55
|
+
[loginTelegram, closeLoginModal],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!loginModalOpen) return;
|
|
60
|
+
function onMessage(e: MessageEvent) {
|
|
61
|
+
if (e.origin !== "https://oauth.telegram.org") return;
|
|
62
|
+
try {
|
|
63
|
+
const data = typeof e.data === "string" ? JSON.parse(e.data) : e.data;
|
|
64
|
+
if (data?.event === "auth_result" && data.result) {
|
|
65
|
+
const r = data.result;
|
|
66
|
+
handleTelegramAuth({
|
|
67
|
+
id: r.id,
|
|
68
|
+
first_name: r.first_name ?? "",
|
|
69
|
+
last_name: r.last_name ?? undefined,
|
|
70
|
+
username: r.username ?? undefined,
|
|
71
|
+
photo_url: r.photo_url ?? undefined,
|
|
72
|
+
auth_date: r.auth_date,
|
|
73
|
+
hash: r.hash,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
/* ignore non-JSON */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
window.addEventListener("message", onMessage);
|
|
81
|
+
return () => window.removeEventListener("message", onMessage);
|
|
82
|
+
}, [loginModalOpen, handleTelegramAuth]);
|
|
83
|
+
|
|
84
|
+
const openTelegramOAuth = useCallback(() => {
|
|
85
|
+
const origin = encodeURIComponent(window.location.origin);
|
|
86
|
+
const url = `https://oauth.telegram.org/auth?bot_id=${botId}&origin=${origin}&request_access=write`;
|
|
87
|
+
const w = 550;
|
|
88
|
+
const h = 470;
|
|
89
|
+
const left = window.screenX + (window.outerWidth - w) / 2;
|
|
90
|
+
const top = window.screenY + (window.outerHeight - h) / 2;
|
|
91
|
+
window.open(
|
|
92
|
+
url,
|
|
93
|
+
"telegram_auth",
|
|
94
|
+
`width=${w},height=${h},left=${left},top=${top}`,
|
|
95
|
+
);
|
|
96
|
+
}, [botId]);
|
|
97
|
+
|
|
98
|
+
const isMobile = useIsMobile();
|
|
99
|
+
|
|
100
|
+
const modalContent = (
|
|
101
|
+
<>
|
|
102
|
+
<div className="flex w-full flex-col items-center gap-2 overflow-hidden">
|
|
103
|
+
<button type="button" onClick={openTelegramOAuth} className={btnClass}>
|
|
104
|
+
<TelegramColorIcon className="size-5" />
|
|
105
|
+
Telegram
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div className="h-px w-full bg-white/5" />
|
|
110
|
+
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={() => {
|
|
114
|
+
closeLoginModal();
|
|
115
|
+
onConnectWallet();
|
|
116
|
+
}}
|
|
117
|
+
className={btnClass}
|
|
118
|
+
>
|
|
119
|
+
{walletIcon ?? <MetaMaskColorIcon className="size-5" />}
|
|
120
|
+
Wallet
|
|
121
|
+
</button>
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<AnimatePresence>
|
|
127
|
+
{loginModalOpen &&
|
|
128
|
+
(isMobile ? (
|
|
129
|
+
<MobileDrawer key="drawer" onClose={closeLoginModal}>
|
|
130
|
+
{modalContent}
|
|
131
|
+
</MobileDrawer>
|
|
132
|
+
) : (
|
|
133
|
+
<>
|
|
134
|
+
<motion.div
|
|
135
|
+
key="backdrop"
|
|
136
|
+
className="fixed inset-0 z-[100] bg-black/60 backdrop-blur-[2px]"
|
|
137
|
+
initial={{ opacity: 0 }}
|
|
138
|
+
animate={{ opacity: 1 }}
|
|
139
|
+
exit={{ opacity: 0 }}
|
|
140
|
+
transition={{ duration: 0.15 }}
|
|
141
|
+
onClick={closeLoginModal}
|
|
142
|
+
/>
|
|
143
|
+
<motion.div
|
|
144
|
+
key="modal-wrapper"
|
|
145
|
+
className="fixed inset-0 z-[101] flex items-center justify-center p-4"
|
|
146
|
+
initial={{ opacity: 0 }}
|
|
147
|
+
animate={{ opacity: 1 }}
|
|
148
|
+
exit={{ opacity: 0 }}
|
|
149
|
+
transition={{ duration: 0.05 }}
|
|
150
|
+
onClick={closeLoginModal}
|
|
151
|
+
>
|
|
152
|
+
<motion.div
|
|
153
|
+
className="flex w-[400px] flex-col items-center gap-4 overflow-hidden rounded-xl border border-white/10 bg-[#282828] p-6"
|
|
154
|
+
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
|
155
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
156
|
+
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
|
157
|
+
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
158
|
+
onClick={(e) => e.stopPropagation()}
|
|
159
|
+
>
|
|
160
|
+
<p className="text-base font-bold tracking-tight text-white">
|
|
161
|
+
Connect
|
|
162
|
+
</p>
|
|
163
|
+
{modalContent}
|
|
164
|
+
</motion.div>
|
|
165
|
+
</motion.div>
|
|
166
|
+
</>
|
|
167
|
+
))}
|
|
168
|
+
</AnimatePresence>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function MobileDrawer({
|
|
173
|
+
children,
|
|
174
|
+
onClose,
|
|
175
|
+
}: {
|
|
176
|
+
children: ReactNode;
|
|
177
|
+
onClose: () => void;
|
|
178
|
+
}) {
|
|
179
|
+
const controls = useAnimationControls();
|
|
180
|
+
|
|
181
|
+
const handleDragEnd = useCallback(
|
|
182
|
+
(_: unknown, info: PanInfo) => {
|
|
183
|
+
if (info.offset.y > 100 || info.velocity.y > 500) {
|
|
184
|
+
onClose();
|
|
185
|
+
} else {
|
|
186
|
+
controls.start({ y: 0 });
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
[onClose, controls],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<>
|
|
194
|
+
<motion.div
|
|
195
|
+
key="drawer-backdrop"
|
|
196
|
+
className="fixed inset-0 z-[100] bg-black/60 backdrop-blur-[2px]"
|
|
197
|
+
initial={{ opacity: 0 }}
|
|
198
|
+
animate={{ opacity: 1 }}
|
|
199
|
+
exit={{ opacity: 0 }}
|
|
200
|
+
transition={{ duration: 0.15 }}
|
|
201
|
+
onClick={onClose}
|
|
202
|
+
/>
|
|
203
|
+
|
|
204
|
+
<motion.div
|
|
205
|
+
key="drawer-sheet"
|
|
206
|
+
className="fixed inset-x-0 bottom-0 z-[101] flex flex-col items-center gap-4 rounded-t-xl border-x border-t border-white/10 bg-[#282828] px-6 pb-[max(24px,env(safe-area-inset-bottom))] pt-3"
|
|
207
|
+
initial={{ y: "100%" }}
|
|
208
|
+
animate={{ y: 0 }}
|
|
209
|
+
exit={{ y: "100%" }}
|
|
210
|
+
transition={{ type: "tween", duration: 0.3, ease: [0.32, 0.72, 0, 1] }}
|
|
211
|
+
drag="y"
|
|
212
|
+
dragConstraints={{ top: 0, bottom: 0 }}
|
|
213
|
+
dragElastic={{ top: 0, bottom: 0.4 }}
|
|
214
|
+
onDragEnd={handleDragEnd}
|
|
215
|
+
>
|
|
216
|
+
<div className="absolute inset-x-0 top-0 bottom-[-100vh] -z-10 bg-[#282828] rounded-t-xl" />
|
|
217
|
+
|
|
218
|
+
<div className="w-full cursor-grab pt-0 pb-1 active:cursor-grabbing">
|
|
219
|
+
<div className="mx-auto h-1 w-[100px] rounded-full bg-white/5" />
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<p className="text-base font-bold tracking-tight text-white">
|
|
223
|
+
Connect
|
|
224
|
+
</p>
|
|
225
|
+
|
|
226
|
+
{children}
|
|
227
|
+
</motion.div>
|
|
228
|
+
</>
|
|
229
|
+
);
|
|
230
|
+
}
|
package/src/agent.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { StoredAgent } from "./types";
|
|
2
|
+
|
|
3
|
+
const AGENT_STORAGE_PREFIX = "hypurr-connect-agent";
|
|
4
|
+
|
|
5
|
+
function storageKey(masterAddress: string): string {
|
|
6
|
+
return `${AGENT_STORAGE_PREFIX}:${masterAddress.toLowerCase()}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function loadAgent(masterAddress: string): StoredAgent | null {
|
|
10
|
+
try {
|
|
11
|
+
const raw = localStorage.getItem(storageKey(masterAddress));
|
|
12
|
+
return raw ? JSON.parse(raw) : null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function saveAgent(masterAddress: string, agent: StoredAgent): void {
|
|
19
|
+
localStorage.setItem(storageKey(masterAddress), JSON.stringify(agent));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function clearAgent(masterAddress: string): void {
|
|
23
|
+
localStorage.removeItem(storageKey(masterAddress));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a random 32-byte private key and derive its address using the
|
|
28
|
+
* SDK's PrivateKeySigner (no viem dependency needed).
|
|
29
|
+
*/
|
|
30
|
+
export async function generateAgentKey(): Promise<{
|
|
31
|
+
privateKey: `0x${string}`;
|
|
32
|
+
address: `0x${string}`;
|
|
33
|
+
}> {
|
|
34
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
35
|
+
const hex = Array.from(bytes)
|
|
36
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
37
|
+
.join("");
|
|
38
|
+
const privateKey = `0x${hex}` as `0x${string}`;
|
|
39
|
+
|
|
40
|
+
const { PrivateKeySigner } = await import("@hfunlabs/hyperliquid/signing");
|
|
41
|
+
const signer = new PrivateKeySigner(privateKey);
|
|
42
|
+
return { privateKey, address: signer.address };
|
|
43
|
+
}
|
package/src/grpc.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
|
|
2
|
+
import { StaticClient } from "hypurr-grpc/ts/hypurr/static/static_service.client";
|
|
3
|
+
import { TelegramClient } from "hypurr-grpc/ts/hypurr/telegram/telegram_service.client";
|
|
4
|
+
import type { HypurrConnectConfig } from "./types";
|
|
5
|
+
|
|
6
|
+
const GRPC_URL = "https://grpc.hypurr.fun";
|
|
7
|
+
|
|
8
|
+
function createTransport(config: HypurrConnectConfig) {
|
|
9
|
+
return new GrpcWebFetchTransport({
|
|
10
|
+
baseUrl: GRPC_URL,
|
|
11
|
+
timeout: config.grpcTimeout ?? 15_000,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createTelegramClient(
|
|
16
|
+
config: HypurrConnectConfig,
|
|
17
|
+
): TelegramClient {
|
|
18
|
+
return new TelegramClient(createTransport(config));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createStaticClient(config: HypurrConnectConfig): StaticClient {
|
|
22
|
+
return new StaticClient(createTransport(config));
|
|
23
|
+
}
|