@hfunlabs/hypurr-connect 0.1.2 → 0.1.4
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 +156 -63
- package/dist/index.d.ts +49 -6
- package/dist/index.js +276 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/HypurrConnectProvider.tsx +271 -38
- package/src/LoginModal.tsx +20 -6
- package/src/TelegramLoginWidget.tsx +62 -0
- package/src/index.ts +8 -0
- package/src/types.ts +53 -6
|
@@ -3,8 +3,7 @@ import {
|
|
|
3
3
|
HttpTransport,
|
|
4
4
|
type IRequestTransport,
|
|
5
5
|
} from "@hfunlabs/hyperliquid";
|
|
6
|
-
import {
|
|
7
|
-
import { PrivateKeySigner } from "@hfunlabs/hyperliquid/signing";
|
|
6
|
+
import { PrivateKeySigner, signUserSignedAction } from "@hfunlabs/hyperliquid/signing";
|
|
8
7
|
import type { TelegramUserResponse } from "hypurr-grpc/ts/hypurr/telegram/telegram_service";
|
|
9
8
|
import type {
|
|
10
9
|
TelegramUser as HypurrTelegramUser,
|
|
@@ -35,6 +34,7 @@ import { createStaticClient, createTelegramClient } from "./grpc";
|
|
|
35
34
|
import { GrpcExchangeTransport } from "./GrpcExchangeTransport";
|
|
36
35
|
import type {
|
|
37
36
|
AuthMethod,
|
|
37
|
+
EoaSigner,
|
|
38
38
|
HypurrConnectConfig,
|
|
39
39
|
HypurrConnectState,
|
|
40
40
|
HypurrUser,
|
|
@@ -46,6 +46,8 @@ import type {
|
|
|
46
46
|
/** @internal context value — extends the public type with fields used only by library internals */
|
|
47
47
|
interface InternalConnectState extends HypurrConnectState {
|
|
48
48
|
loginTelegram: (data: TelegramLoginData) => void;
|
|
49
|
+
botUsername: string;
|
|
50
|
+
useWidget: boolean;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
const TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-user";
|
|
@@ -125,9 +127,19 @@ export function HypurrConnectProvider({
|
|
|
125
127
|
(async () => {
|
|
126
128
|
try {
|
|
127
129
|
const authData = toAuthDataMap(tgLoginData);
|
|
128
|
-
const { response }
|
|
130
|
+
const [{ response: userResp }, { response: walletsResp }] =
|
|
131
|
+
await Promise.all([
|
|
132
|
+
tgClient.telegramUser({ authData }),
|
|
133
|
+
tgClient.telegramUserWallets({ authData }),
|
|
134
|
+
]);
|
|
129
135
|
if (cancelled) return;
|
|
130
|
-
|
|
136
|
+
const user = (userResp as TelegramUserResponse).user ?? null;
|
|
137
|
+
if (user) {
|
|
138
|
+
// TelegramUser.wallets lacks twap/scale sessions; replace with
|
|
139
|
+
// the full wallets from TelegramUserWallets which populates them.
|
|
140
|
+
user.wallets = walletsResp.wallets;
|
|
141
|
+
}
|
|
142
|
+
setTgUser(user);
|
|
131
143
|
} catch (err) {
|
|
132
144
|
if (cancelled) return;
|
|
133
145
|
console.error("[HypurrConnect] gRPC TelegramUser failed:", err);
|
|
@@ -147,6 +159,7 @@ export function HypurrConnectProvider({
|
|
|
147
159
|
const [agent, setAgent] = useState<StoredAgent | null>(null);
|
|
148
160
|
const [eoaLoading, setEoaLoading] = useState(false);
|
|
149
161
|
const [eoaError, setEoaError] = useState<string | null>(null);
|
|
162
|
+
const eoaSignerRef = useRef<EoaSigner | null>(null);
|
|
150
163
|
|
|
151
164
|
// ── Derived auth ─────────────────────────────────────────────
|
|
152
165
|
const authMethod: AuthMethod = tgLoginData
|
|
@@ -223,7 +236,12 @@ export function HypurrConnectProvider({
|
|
|
223
236
|
|
|
224
237
|
// ── Exchange client ──────────────────────────────────────────
|
|
225
238
|
// Telegram: GrpcExchangeTransport → HyperliquidCoreAction (server signs)
|
|
226
|
-
// EOA:
|
|
239
|
+
// EOA: dual wallet — agent key for L1 actions, master signer for user-signed
|
|
240
|
+
// actions (transfers, withdrawals, etc.). The dual wallet inspects the
|
|
241
|
+
// EIP-712 domain name to decide which key signs each request.
|
|
242
|
+
// When a signer is available but no agent exists yet, the dual wallet
|
|
243
|
+
// auto-provisions an agent on the first L1 action (triggers one extra
|
|
244
|
+
// wallet popup for the approveAgent user-signed action).
|
|
227
245
|
|
|
228
246
|
const onDeadAgentRef = useRef<((address: `0x${string}`) => void) | null>(
|
|
229
247
|
null,
|
|
@@ -234,6 +252,20 @@ export function HypurrConnectProvider({
|
|
|
234
252
|
setEoaError("Agent expired or was deregistered. Please reconnect.");
|
|
235
253
|
};
|
|
236
254
|
|
|
255
|
+
// Mutable slot for the agent signer — the dual wallet reads this so it can
|
|
256
|
+
// pick up a newly provisioned agent without waiting for a React re-render.
|
|
257
|
+
const agentSignerRef = useRef<PrivateKeySigner | null>(
|
|
258
|
+
agent ? new PrivateKeySigner(agent.privateKey) : null,
|
|
259
|
+
);
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
agentSignerRef.current = agent
|
|
262
|
+
? new PrivateKeySigner(agent.privateKey)
|
|
263
|
+
: null;
|
|
264
|
+
}, [agent]);
|
|
265
|
+
|
|
266
|
+
// Lock to prevent concurrent auto-provisioning attempts
|
|
267
|
+
const provisioningRef = useRef<Promise<PrivateKeySigner> | null>(null);
|
|
268
|
+
|
|
237
269
|
const agentReady =
|
|
238
270
|
authMethod === "telegram" || (authMethod === "eoa" && !!agent);
|
|
239
271
|
|
|
@@ -254,14 +286,16 @@ export function HypurrConnectProvider({
|
|
|
254
286
|
}
|
|
255
287
|
|
|
256
288
|
if (authMethod === "eoa" && eoaAddress) {
|
|
257
|
-
|
|
289
|
+
const hasSigner = !!eoaSignerRef.current;
|
|
290
|
+
|
|
291
|
+
if (!agent && !hasSigner) {
|
|
258
292
|
const noAgentTransport: IRequestTransport = {
|
|
259
293
|
isTestnet: config.isTestnet ?? false,
|
|
260
294
|
request(): Promise<never> {
|
|
261
295
|
throw new Error(
|
|
262
|
-
"[HypurrConnect] No agent key approved. " +
|
|
263
|
-
"
|
|
264
|
-
"
|
|
296
|
+
"[HypurrConnect] No agent key approved and no wallet signer available. " +
|
|
297
|
+
"Either call approveAgent(signTypedDataAsync) or pass a signer to " +
|
|
298
|
+
"connectEoa(address, { signTypedData, chainId }).",
|
|
265
299
|
);
|
|
266
300
|
},
|
|
267
301
|
};
|
|
@@ -272,9 +306,8 @@ export function HypurrConnectProvider({
|
|
|
272
306
|
});
|
|
273
307
|
}
|
|
274
308
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
});
|
|
309
|
+
const isTestnet = config.isTestnet ?? false;
|
|
310
|
+
const inner = new HttpTransport({ isTestnet });
|
|
278
311
|
const deadAgentAddr = eoaAddress;
|
|
279
312
|
const guardedTransport: IRequestTransport = {
|
|
280
313
|
isTestnet: inner.isTestnet,
|
|
@@ -293,10 +326,160 @@ export function HypurrConnectProvider({
|
|
|
293
326
|
}
|
|
294
327
|
},
|
|
295
328
|
};
|
|
296
|
-
|
|
329
|
+
|
|
330
|
+
const signerRef = eoaSignerRef;
|
|
331
|
+
const agentRef = agentSignerRef;
|
|
332
|
+
const provRef = provisioningRef;
|
|
333
|
+
const ownerAddress = eoaAddress;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Auto-provision an agent key when one doesn't exist yet.
|
|
337
|
+
*
|
|
338
|
+
* Bypasses the SDK's `executeUserSignedAction` (and its per-address
|
|
339
|
+
* semaphore) to avoid deadlocking when called from inside
|
|
340
|
+
* `dualWallet.signTypedData`, which is already inside the SDK's
|
|
341
|
+
* `executeL1Action` lock for the same address.
|
|
342
|
+
*/
|
|
343
|
+
const ensureAgent = async (): Promise<PrivateKeySigner> => {
|
|
344
|
+
const existing = agentRef.current;
|
|
345
|
+
if (existing) return existing;
|
|
346
|
+
|
|
347
|
+
if (provRef.current) return provRef.current;
|
|
348
|
+
|
|
349
|
+
const signer = signerRef.current;
|
|
350
|
+
if (!signer) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
"[HypurrConnect] No wallet signer available to auto-provision agent. " +
|
|
353
|
+
"Pass a signer to connectEoa(address, { signTypedData, chainId }).",
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
provRef.current = (async () => {
|
|
358
|
+
try {
|
|
359
|
+
const { privateKey, address: agentAddress } =
|
|
360
|
+
await generateAgentKey();
|
|
361
|
+
|
|
362
|
+
const chainIdHex = `0x${signer.chainId.toString(16)}` as `0x${string}`;
|
|
363
|
+
const nonce = Date.now();
|
|
364
|
+
const action = {
|
|
365
|
+
type: "approveAgent" as const,
|
|
366
|
+
signatureChainId: chainIdHex,
|
|
367
|
+
hyperliquidChain: (isTestnet ? "Testnet" : "Mainnet") as
|
|
368
|
+
| "Testnet"
|
|
369
|
+
| "Mainnet",
|
|
370
|
+
agentAddress: agentAddress.toLowerCase() as `0x${string}`,
|
|
371
|
+
agentName: AGENT_NAME,
|
|
372
|
+
nonce,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const approveAgentTypes = {
|
|
376
|
+
"HyperliquidTransaction:ApproveAgent": [
|
|
377
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
378
|
+
{ name: "agentAddress", type: "address" },
|
|
379
|
+
{ name: "agentName", type: "string" },
|
|
380
|
+
{ name: "nonce", type: "uint64" },
|
|
381
|
+
],
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const wallet = {
|
|
385
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
386
|
+
signTypedData(params: any) {
|
|
387
|
+
return signer.signTypedData(params);
|
|
388
|
+
},
|
|
389
|
+
getAddresses: async () => [ownerAddress] as `0x${string}`[],
|
|
390
|
+
getChainId: async () => signer.chainId,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const signature = await signUserSignedAction({
|
|
394
|
+
wallet,
|
|
395
|
+
action,
|
|
396
|
+
types: approveAgentTypes,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const apiUrl = isTestnet
|
|
400
|
+
? "https://api.hyperliquid-testnet.xyz/exchange"
|
|
401
|
+
: "https://api.hyperliquid.xyz/exchange";
|
|
402
|
+
|
|
403
|
+
const res = await fetch(apiUrl, {
|
|
404
|
+
method: "POST",
|
|
405
|
+
headers: { "Content-Type": "application/json" },
|
|
406
|
+
body: JSON.stringify({ action, signature, nonce }),
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const body = await res.json();
|
|
410
|
+
if (body?.status === "err") {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`approveAgent API error: ${body.response ?? JSON.stringify(body)}`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const remote = await fetchActiveAgent(ownerAddress, isTestnet);
|
|
417
|
+
const validUntil =
|
|
418
|
+
remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1000;
|
|
419
|
+
|
|
420
|
+
const stored: StoredAgent = {
|
|
421
|
+
privateKey,
|
|
422
|
+
address: agentAddress,
|
|
423
|
+
approvedAt: Date.now(),
|
|
424
|
+
validUntil,
|
|
425
|
+
};
|
|
426
|
+
saveAgent(ownerAddress, stored);
|
|
427
|
+
|
|
428
|
+
const newSigner = new PrivateKeySigner(privateKey);
|
|
429
|
+
agentRef.current = newSigner;
|
|
430
|
+
setAgent(stored);
|
|
431
|
+
|
|
432
|
+
return newSigner;
|
|
433
|
+
} finally {
|
|
434
|
+
provRef.current = null;
|
|
435
|
+
}
|
|
436
|
+
})();
|
|
437
|
+
|
|
438
|
+
return provRef.current;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// Dual wallet: routes signing based on the EIP-712 domain.
|
|
442
|
+
// "Exchange" domain → L1 action → agent key signs (auto-provisions if needed).
|
|
443
|
+
// "HyperliquidSignTransaction" domain → user-signed → master wallet (popup).
|
|
444
|
+
const dualWallet = {
|
|
445
|
+
address: ownerAddress,
|
|
446
|
+
async signTypedData(params: {
|
|
447
|
+
domain: {
|
|
448
|
+
name?: string;
|
|
449
|
+
version?: string;
|
|
450
|
+
chainId?: number;
|
|
451
|
+
verifyingContract?: `0x${string}`;
|
|
452
|
+
salt?: `0x${string}`;
|
|
453
|
+
};
|
|
454
|
+
types: Record<string, { name: string; type: string }[]>;
|
|
455
|
+
primaryType: string;
|
|
456
|
+
message: Record<string, unknown>;
|
|
457
|
+
}): Promise<`0x${string}`> {
|
|
458
|
+
if (params.domain.name === "HyperliquidSignTransaction") {
|
|
459
|
+
const signer = signerRef.current;
|
|
460
|
+
if (!signer) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
"[HypurrConnect] No wallet signer available for user-signed actions. " +
|
|
463
|
+
"Pass a signer to connectEoa(address, { signTypedData, chainId }).",
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return signer.signTypedData(
|
|
467
|
+
params as Parameters<typeof signer.signTypedData>[0],
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const agentSigner = await ensureAgent();
|
|
472
|
+
return agentSigner.signTypedData(params);
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
|
|
297
476
|
return new ExchangeClient({
|
|
298
477
|
transport: guardedTransport,
|
|
299
|
-
wallet,
|
|
478
|
+
wallet: dualWallet,
|
|
479
|
+
signatureChainId: () => {
|
|
480
|
+
const id = signerRef.current?.chainId ?? 42161;
|
|
481
|
+
return `0x${id.toString(16)}` as `0x${string}`;
|
|
482
|
+
},
|
|
300
483
|
});
|
|
301
484
|
}
|
|
302
485
|
|
|
@@ -415,22 +598,26 @@ export function HypurrConnectProvider({
|
|
|
415
598
|
setEoaError(null);
|
|
416
599
|
}, []);
|
|
417
600
|
|
|
418
|
-
const connectEoa = useCallback(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
601
|
+
const connectEoa = useCallback(
|
|
602
|
+
(address: `0x${string}`, signer?: EoaSigner) => {
|
|
603
|
+
eoaSignerRef.current = signer ?? null;
|
|
604
|
+
setEoaAddress(address);
|
|
605
|
+
setTgLoginData(null);
|
|
606
|
+
setTgUser(null);
|
|
607
|
+
setTgError(null);
|
|
608
|
+
setEoaError(null);
|
|
609
|
+
localStorage.removeItem(TELEGRAM_STORAGE_KEY);
|
|
425
610
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
611
|
+
const existing = loadAgent(address);
|
|
612
|
+
if (existing && existing.validUntil > Date.now()) {
|
|
613
|
+
setAgent(existing);
|
|
614
|
+
} else {
|
|
615
|
+
if (existing) clearStoredAgent(address);
|
|
616
|
+
setAgent(null);
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
[],
|
|
620
|
+
);
|
|
434
621
|
|
|
435
622
|
const approveAgentFn = useCallback(
|
|
436
623
|
async (signTypedDataAsync: SignTypedDataFn, chainId: number) => {
|
|
@@ -440,6 +627,8 @@ export function HypurrConnectProvider({
|
|
|
440
627
|
);
|
|
441
628
|
}
|
|
442
629
|
|
|
630
|
+
eoaSignerRef.current = { signTypedData: signTypedDataAsync, chainId };
|
|
631
|
+
|
|
443
632
|
setEoaLoading(true);
|
|
444
633
|
setEoaError(null);
|
|
445
634
|
try {
|
|
@@ -457,20 +646,59 @@ export function HypurrConnectProvider({
|
|
|
457
646
|
const { privateKey, address: agentAddress } = await generateAgentKey();
|
|
458
647
|
const isTestnet = config.isTestnet ?? false;
|
|
459
648
|
|
|
649
|
+
const chainIdHex = `0x${chainId.toString(16)}` as `0x${string}`;
|
|
650
|
+
const nonce = Date.now();
|
|
651
|
+
const action = {
|
|
652
|
+
type: "approveAgent" as const,
|
|
653
|
+
signatureChainId: chainIdHex,
|
|
654
|
+
hyperliquidChain: (isTestnet ? "Testnet" : "Mainnet") as
|
|
655
|
+
| "Testnet"
|
|
656
|
+
| "Mainnet",
|
|
657
|
+
agentAddress: agentAddress.toLowerCase() as `0x${string}`,
|
|
658
|
+
agentName: AGENT_NAME,
|
|
659
|
+
nonce,
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const approveAgentTypes = {
|
|
663
|
+
"HyperliquidTransaction:ApproveAgent": [
|
|
664
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
665
|
+
{ name: "agentAddress", type: "address" },
|
|
666
|
+
{ name: "agentName", type: "string" },
|
|
667
|
+
{ name: "nonce", type: "uint64" },
|
|
668
|
+
],
|
|
669
|
+
};
|
|
670
|
+
|
|
460
671
|
const wallet = {
|
|
461
|
-
|
|
672
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
673
|
+
signTypedData(params: any) {
|
|
674
|
+
return signTypedDataAsync(params);
|
|
675
|
+
},
|
|
462
676
|
getAddresses: async () => [eoaAddress] as `0x${string}`[],
|
|
463
677
|
getChainId: async () => chainId,
|
|
464
678
|
};
|
|
465
|
-
const transport = new HttpTransport({ isTestnet });
|
|
466
679
|
|
|
467
|
-
await
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
680
|
+
const signature = await signUserSignedAction({
|
|
681
|
+
wallet,
|
|
682
|
+
action,
|
|
683
|
+
types: approveAgentTypes,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const apiUrl = isTestnet
|
|
687
|
+
? "https://api.hyperliquid-testnet.xyz/exchange"
|
|
688
|
+
: "https://api.hyperliquid.xyz/exchange";
|
|
689
|
+
|
|
690
|
+
const res = await fetch(apiUrl, {
|
|
691
|
+
method: "POST",
|
|
692
|
+
headers: { "Content-Type": "application/json" },
|
|
693
|
+
body: JSON.stringify({ action, signature, nonce }),
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const body = await res.json();
|
|
697
|
+
if (body?.status === "err") {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`approveAgent API error: ${body.response ?? JSON.stringify(body)}`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
474
702
|
|
|
475
703
|
const remote = await fetchActiveAgent(eoaAddress, isTestnet);
|
|
476
704
|
const validUntil =
|
|
@@ -502,6 +730,7 @@ export function HypurrConnectProvider({
|
|
|
502
730
|
setEoaAddress(null);
|
|
503
731
|
setAgent(null);
|
|
504
732
|
setEoaError(null);
|
|
733
|
+
eoaSignerRef.current = null;
|
|
505
734
|
localStorage.removeItem(TELEGRAM_STORAGE_KEY);
|
|
506
735
|
}, []);
|
|
507
736
|
|
|
@@ -543,6 +772,8 @@ export function HypurrConnectProvider({
|
|
|
543
772
|
clearAgent: handleClearAgent,
|
|
544
773
|
|
|
545
774
|
botId: config.telegram?.botId ?? "",
|
|
775
|
+
botUsername: config.telegram?.botUsername ?? "",
|
|
776
|
+
useWidget: config.telegram?.useWidget ?? false,
|
|
546
777
|
|
|
547
778
|
authDataMap,
|
|
548
779
|
telegramClient: tgClient,
|
|
@@ -578,6 +809,8 @@ export function HypurrConnectProvider({
|
|
|
578
809
|
agentReady,
|
|
579
810
|
handleClearAgent,
|
|
580
811
|
config.telegram?.botId,
|
|
812
|
+
config.telegram?.botUsername,
|
|
813
|
+
config.telegram?.useWidget,
|
|
581
814
|
authDataMap,
|
|
582
815
|
tgClient,
|
|
583
816
|
staticClient,
|
package/src/LoginModal.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
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/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,
|
|
@@ -20,3 +24,7 @@ export type {
|
|
|
20
24
|
// Re-export wallet types from hypurr-grpc so consumers don't need the dependency
|
|
21
25
|
export type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
|
|
22
26
|
export type { TelegramChatWalletPack } from "hypurr-grpc/ts/hypurr/user";
|
|
27
|
+
export type {
|
|
28
|
+
HyperliquidWalletScaleSession,
|
|
29
|
+
HyperliquidWalletTwapSession,
|
|
30
|
+
} from "hypurr-grpc/ts/hypurr/tools";
|
package/src/types.ts
CHANGED
|
@@ -1,17 +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 { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
|
|
5
4
|
import type { TelegramChatWalletPack } from "hypurr-grpc/ts/hypurr/user";
|
|
5
|
+
import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
|
|
6
6
|
|
|
7
7
|
// ─── Config ──────────────────────────────────────────────────────
|
|
8
8
|
|
|
9
9
|
export interface HypurrConnectConfig {
|
|
10
10
|
grpcTimeout?: number;
|
|
11
11
|
isTestnet?: boolean;
|
|
12
|
-
telegram
|
|
12
|
+
telegram: {
|
|
13
13
|
botUsername: string;
|
|
14
|
-
botId
|
|
14
|
+
botId?: string;
|
|
15
|
+
useWidget: boolean;
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -59,6 +60,51 @@ export type SignTypedDataFn = (params: {
|
|
|
59
60
|
message: Record<string, unknown>;
|
|
60
61
|
}) => Promise<`0x${string}`>;
|
|
61
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
|
+
|
|
62
108
|
// ─── Context state ───────────────────────────────────────────────
|
|
63
109
|
|
|
64
110
|
export interface HypurrConnectState {
|
|
@@ -69,9 +115,10 @@ export interface HypurrConnectState {
|
|
|
69
115
|
error: string | null;
|
|
70
116
|
authMethod: AuthMethod;
|
|
71
117
|
|
|
72
|
-
// SDK ExchangeClient —
|
|
118
|
+
// SDK ExchangeClient — handles both L1 (agent-signed) and user-signed actions.
|
|
73
119
|
// Telegram: backed by GrpcExchangeTransport (HyperliquidCoreAction)
|
|
74
|
-
// 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.
|
|
75
122
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
123
|
exchange: ExchangeClient<any> | null;
|
|
77
124
|
|
|
@@ -109,7 +156,7 @@ export interface HypurrConnectState {
|
|
|
109
156
|
closeLoginModal: () => void;
|
|
110
157
|
|
|
111
158
|
// Auth actions
|
|
112
|
-
connectEoa: (address: `0x${string}
|
|
159
|
+
connectEoa: (address: `0x${string}`, signer?: EoaSigner) => void;
|
|
113
160
|
approveAgent: (
|
|
114
161
|
signTypedDataAsync: SignTypedDataFn,
|
|
115
162
|
chainId: number,
|