@hfunlabs/hypurr-connect 0.1.2 → 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 +156 -63
- package/dist/index.d.ts +48 -6
- package/dist/index.js +267 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/HypurrConnectProvider.tsx +259 -36
- package/src/LoginModal.tsx +20 -6
- package/src/TelegramLoginWidget.tsx +62 -0
- package/src/index.ts +4 -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";
|
|
@@ -147,6 +149,7 @@ export function HypurrConnectProvider({
|
|
|
147
149
|
const [agent, setAgent] = useState<StoredAgent | null>(null);
|
|
148
150
|
const [eoaLoading, setEoaLoading] = useState(false);
|
|
149
151
|
const [eoaError, setEoaError] = useState<string | null>(null);
|
|
152
|
+
const eoaSignerRef = useRef<EoaSigner | null>(null);
|
|
150
153
|
|
|
151
154
|
// ── Derived auth ─────────────────────────────────────────────
|
|
152
155
|
const authMethod: AuthMethod = tgLoginData
|
|
@@ -223,7 +226,12 @@ export function HypurrConnectProvider({
|
|
|
223
226
|
|
|
224
227
|
// ── Exchange client ──────────────────────────────────────────
|
|
225
228
|
// Telegram: GrpcExchangeTransport → HyperliquidCoreAction (server signs)
|
|
226
|
-
// EOA:
|
|
229
|
+
// EOA: dual wallet — agent key for L1 actions, master signer for user-signed
|
|
230
|
+
// actions (transfers, withdrawals, etc.). The dual wallet inspects the
|
|
231
|
+
// EIP-712 domain name to decide which key signs each request.
|
|
232
|
+
// When a signer is available but no agent exists yet, the dual wallet
|
|
233
|
+
// auto-provisions an agent on the first L1 action (triggers one extra
|
|
234
|
+
// wallet popup for the approveAgent user-signed action).
|
|
227
235
|
|
|
228
236
|
const onDeadAgentRef = useRef<((address: `0x${string}`) => void) | null>(
|
|
229
237
|
null,
|
|
@@ -234,6 +242,20 @@ export function HypurrConnectProvider({
|
|
|
234
242
|
setEoaError("Agent expired or was deregistered. Please reconnect.");
|
|
235
243
|
};
|
|
236
244
|
|
|
245
|
+
// Mutable slot for the agent signer — the dual wallet reads this so it can
|
|
246
|
+
// pick up a newly provisioned agent without waiting for a React re-render.
|
|
247
|
+
const agentSignerRef = useRef<PrivateKeySigner | null>(
|
|
248
|
+
agent ? new PrivateKeySigner(agent.privateKey) : null,
|
|
249
|
+
);
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
agentSignerRef.current = agent
|
|
252
|
+
? new PrivateKeySigner(agent.privateKey)
|
|
253
|
+
: null;
|
|
254
|
+
}, [agent]);
|
|
255
|
+
|
|
256
|
+
// Lock to prevent concurrent auto-provisioning attempts
|
|
257
|
+
const provisioningRef = useRef<Promise<PrivateKeySigner> | null>(null);
|
|
258
|
+
|
|
237
259
|
const agentReady =
|
|
238
260
|
authMethod === "telegram" || (authMethod === "eoa" && !!agent);
|
|
239
261
|
|
|
@@ -254,14 +276,16 @@ export function HypurrConnectProvider({
|
|
|
254
276
|
}
|
|
255
277
|
|
|
256
278
|
if (authMethod === "eoa" && eoaAddress) {
|
|
257
|
-
|
|
279
|
+
const hasSigner = !!eoaSignerRef.current;
|
|
280
|
+
|
|
281
|
+
if (!agent && !hasSigner) {
|
|
258
282
|
const noAgentTransport: IRequestTransport = {
|
|
259
283
|
isTestnet: config.isTestnet ?? false,
|
|
260
284
|
request(): Promise<never> {
|
|
261
285
|
throw new Error(
|
|
262
|
-
"[HypurrConnect] No agent key approved. " +
|
|
263
|
-
"
|
|
264
|
-
"
|
|
286
|
+
"[HypurrConnect] No agent key approved and no wallet signer available. " +
|
|
287
|
+
"Either call approveAgent(signTypedDataAsync) or pass a signer to " +
|
|
288
|
+
"connectEoa(address, { signTypedData, chainId }).",
|
|
265
289
|
);
|
|
266
290
|
},
|
|
267
291
|
};
|
|
@@ -272,9 +296,8 @@ export function HypurrConnectProvider({
|
|
|
272
296
|
});
|
|
273
297
|
}
|
|
274
298
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
});
|
|
299
|
+
const isTestnet = config.isTestnet ?? false;
|
|
300
|
+
const inner = new HttpTransport({ isTestnet });
|
|
278
301
|
const deadAgentAddr = eoaAddress;
|
|
279
302
|
const guardedTransport: IRequestTransport = {
|
|
280
303
|
isTestnet: inner.isTestnet,
|
|
@@ -293,10 +316,160 @@ export function HypurrConnectProvider({
|
|
|
293
316
|
}
|
|
294
317
|
},
|
|
295
318
|
};
|
|
296
|
-
|
|
319
|
+
|
|
320
|
+
const signerRef = eoaSignerRef;
|
|
321
|
+
const agentRef = agentSignerRef;
|
|
322
|
+
const provRef = provisioningRef;
|
|
323
|
+
const ownerAddress = eoaAddress;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Auto-provision an agent key when one doesn't exist yet.
|
|
327
|
+
*
|
|
328
|
+
* Bypasses the SDK's `executeUserSignedAction` (and its per-address
|
|
329
|
+
* semaphore) to avoid deadlocking when called from inside
|
|
330
|
+
* `dualWallet.signTypedData`, which is already inside the SDK's
|
|
331
|
+
* `executeL1Action` lock for the same address.
|
|
332
|
+
*/
|
|
333
|
+
const ensureAgent = async (): Promise<PrivateKeySigner> => {
|
|
334
|
+
const existing = agentRef.current;
|
|
335
|
+
if (existing) return existing;
|
|
336
|
+
|
|
337
|
+
if (provRef.current) return provRef.current;
|
|
338
|
+
|
|
339
|
+
const signer = signerRef.current;
|
|
340
|
+
if (!signer) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
"[HypurrConnect] No wallet signer available to auto-provision agent. " +
|
|
343
|
+
"Pass a signer to connectEoa(address, { signTypedData, chainId }).",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
provRef.current = (async () => {
|
|
348
|
+
try {
|
|
349
|
+
const { privateKey, address: agentAddress } =
|
|
350
|
+
await generateAgentKey();
|
|
351
|
+
|
|
352
|
+
const chainIdHex = `0x${signer.chainId.toString(16)}` as `0x${string}`;
|
|
353
|
+
const nonce = Date.now();
|
|
354
|
+
const action = {
|
|
355
|
+
type: "approveAgent" as const,
|
|
356
|
+
signatureChainId: chainIdHex,
|
|
357
|
+
hyperliquidChain: (isTestnet ? "Testnet" : "Mainnet") as
|
|
358
|
+
| "Testnet"
|
|
359
|
+
| "Mainnet",
|
|
360
|
+
agentAddress: agentAddress.toLowerCase() as `0x${string}`,
|
|
361
|
+
agentName: AGENT_NAME,
|
|
362
|
+
nonce,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const approveAgentTypes = {
|
|
366
|
+
"HyperliquidTransaction:ApproveAgent": [
|
|
367
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
368
|
+
{ name: "agentAddress", type: "address" },
|
|
369
|
+
{ name: "agentName", type: "string" },
|
|
370
|
+
{ name: "nonce", type: "uint64" },
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const wallet = {
|
|
375
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
376
|
+
signTypedData(params: any) {
|
|
377
|
+
return signer.signTypedData(params);
|
|
378
|
+
},
|
|
379
|
+
getAddresses: async () => [ownerAddress] as `0x${string}`[],
|
|
380
|
+
getChainId: async () => signer.chainId,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const signature = await signUserSignedAction({
|
|
384
|
+
wallet,
|
|
385
|
+
action,
|
|
386
|
+
types: approveAgentTypes,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const apiUrl = isTestnet
|
|
390
|
+
? "https://api.hyperliquid-testnet.xyz/exchange"
|
|
391
|
+
: "https://api.hyperliquid.xyz/exchange";
|
|
392
|
+
|
|
393
|
+
const res = await fetch(apiUrl, {
|
|
394
|
+
method: "POST",
|
|
395
|
+
headers: { "Content-Type": "application/json" },
|
|
396
|
+
body: JSON.stringify({ action, signature, nonce }),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const body = await res.json();
|
|
400
|
+
if (body?.status === "err") {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`approveAgent API error: ${body.response ?? JSON.stringify(body)}`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const remote = await fetchActiveAgent(ownerAddress, isTestnet);
|
|
407
|
+
const validUntil =
|
|
408
|
+
remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1000;
|
|
409
|
+
|
|
410
|
+
const stored: StoredAgent = {
|
|
411
|
+
privateKey,
|
|
412
|
+
address: agentAddress,
|
|
413
|
+
approvedAt: Date.now(),
|
|
414
|
+
validUntil,
|
|
415
|
+
};
|
|
416
|
+
saveAgent(ownerAddress, stored);
|
|
417
|
+
|
|
418
|
+
const newSigner = new PrivateKeySigner(privateKey);
|
|
419
|
+
agentRef.current = newSigner;
|
|
420
|
+
setAgent(stored);
|
|
421
|
+
|
|
422
|
+
return newSigner;
|
|
423
|
+
} finally {
|
|
424
|
+
provRef.current = null;
|
|
425
|
+
}
|
|
426
|
+
})();
|
|
427
|
+
|
|
428
|
+
return provRef.current;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// Dual wallet: routes signing based on the EIP-712 domain.
|
|
432
|
+
// "Exchange" domain → L1 action → agent key signs (auto-provisions if needed).
|
|
433
|
+
// "HyperliquidSignTransaction" domain → user-signed → master wallet (popup).
|
|
434
|
+
const dualWallet = {
|
|
435
|
+
address: ownerAddress,
|
|
436
|
+
async signTypedData(params: {
|
|
437
|
+
domain: {
|
|
438
|
+
name?: string;
|
|
439
|
+
version?: string;
|
|
440
|
+
chainId?: number;
|
|
441
|
+
verifyingContract?: `0x${string}`;
|
|
442
|
+
salt?: `0x${string}`;
|
|
443
|
+
};
|
|
444
|
+
types: Record<string, { name: string; type: string }[]>;
|
|
445
|
+
primaryType: string;
|
|
446
|
+
message: Record<string, unknown>;
|
|
447
|
+
}): Promise<`0x${string}`> {
|
|
448
|
+
if (params.domain.name === "HyperliquidSignTransaction") {
|
|
449
|
+
const signer = signerRef.current;
|
|
450
|
+
if (!signer) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
"[HypurrConnect] No wallet signer available for user-signed actions. " +
|
|
453
|
+
"Pass a signer to connectEoa(address, { signTypedData, chainId }).",
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return signer.signTypedData(
|
|
457
|
+
params as Parameters<typeof signer.signTypedData>[0],
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const agentSigner = await ensureAgent();
|
|
462
|
+
return agentSigner.signTypedData(params);
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
|
|
297
466
|
return new ExchangeClient({
|
|
298
467
|
transport: guardedTransport,
|
|
299
|
-
wallet,
|
|
468
|
+
wallet: dualWallet,
|
|
469
|
+
signatureChainId: () => {
|
|
470
|
+
const id = signerRef.current?.chainId ?? 42161;
|
|
471
|
+
return `0x${id.toString(16)}` as `0x${string}`;
|
|
472
|
+
},
|
|
300
473
|
});
|
|
301
474
|
}
|
|
302
475
|
|
|
@@ -415,22 +588,26 @@ export function HypurrConnectProvider({
|
|
|
415
588
|
setEoaError(null);
|
|
416
589
|
}, []);
|
|
417
590
|
|
|
418
|
-
const connectEoa = useCallback(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
591
|
+
const connectEoa = useCallback(
|
|
592
|
+
(address: `0x${string}`, signer?: EoaSigner) => {
|
|
593
|
+
eoaSignerRef.current = signer ?? null;
|
|
594
|
+
setEoaAddress(address);
|
|
595
|
+
setTgLoginData(null);
|
|
596
|
+
setTgUser(null);
|
|
597
|
+
setTgError(null);
|
|
598
|
+
setEoaError(null);
|
|
599
|
+
localStorage.removeItem(TELEGRAM_STORAGE_KEY);
|
|
425
600
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
601
|
+
const existing = loadAgent(address);
|
|
602
|
+
if (existing && existing.validUntil > Date.now()) {
|
|
603
|
+
setAgent(existing);
|
|
604
|
+
} else {
|
|
605
|
+
if (existing) clearStoredAgent(address);
|
|
606
|
+
setAgent(null);
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
[],
|
|
610
|
+
);
|
|
434
611
|
|
|
435
612
|
const approveAgentFn = useCallback(
|
|
436
613
|
async (signTypedDataAsync: SignTypedDataFn, chainId: number) => {
|
|
@@ -440,6 +617,8 @@ export function HypurrConnectProvider({
|
|
|
440
617
|
);
|
|
441
618
|
}
|
|
442
619
|
|
|
620
|
+
eoaSignerRef.current = { signTypedData: signTypedDataAsync, chainId };
|
|
621
|
+
|
|
443
622
|
setEoaLoading(true);
|
|
444
623
|
setEoaError(null);
|
|
445
624
|
try {
|
|
@@ -457,20 +636,59 @@ export function HypurrConnectProvider({
|
|
|
457
636
|
const { privateKey, address: agentAddress } = await generateAgentKey();
|
|
458
637
|
const isTestnet = config.isTestnet ?? false;
|
|
459
638
|
|
|
639
|
+
const chainIdHex = `0x${chainId.toString(16)}` as `0x${string}`;
|
|
640
|
+
const nonce = Date.now();
|
|
641
|
+
const action = {
|
|
642
|
+
type: "approveAgent" as const,
|
|
643
|
+
signatureChainId: chainIdHex,
|
|
644
|
+
hyperliquidChain: (isTestnet ? "Testnet" : "Mainnet") as
|
|
645
|
+
| "Testnet"
|
|
646
|
+
| "Mainnet",
|
|
647
|
+
agentAddress: agentAddress.toLowerCase() as `0x${string}`,
|
|
648
|
+
agentName: AGENT_NAME,
|
|
649
|
+
nonce,
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const approveAgentTypes = {
|
|
653
|
+
"HyperliquidTransaction:ApproveAgent": [
|
|
654
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
655
|
+
{ name: "agentAddress", type: "address" },
|
|
656
|
+
{ name: "agentName", type: "string" },
|
|
657
|
+
{ name: "nonce", type: "uint64" },
|
|
658
|
+
],
|
|
659
|
+
};
|
|
660
|
+
|
|
460
661
|
const wallet = {
|
|
461
|
-
|
|
662
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
663
|
+
signTypedData(params: any) {
|
|
664
|
+
return signTypedDataAsync(params);
|
|
665
|
+
},
|
|
462
666
|
getAddresses: async () => [eoaAddress] as `0x${string}`[],
|
|
463
667
|
getChainId: async () => chainId,
|
|
464
668
|
};
|
|
465
|
-
const transport = new HttpTransport({ isTestnet });
|
|
466
669
|
|
|
467
|
-
await
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
670
|
+
const signature = await signUserSignedAction({
|
|
671
|
+
wallet,
|
|
672
|
+
action,
|
|
673
|
+
types: approveAgentTypes,
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const apiUrl = isTestnet
|
|
677
|
+
? "https://api.hyperliquid-testnet.xyz/exchange"
|
|
678
|
+
: "https://api.hyperliquid.xyz/exchange";
|
|
679
|
+
|
|
680
|
+
const res = await fetch(apiUrl, {
|
|
681
|
+
method: "POST",
|
|
682
|
+
headers: { "Content-Type": "application/json" },
|
|
683
|
+
body: JSON.stringify({ action, signature, nonce }),
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const body = await res.json();
|
|
687
|
+
if (body?.status === "err") {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`approveAgent API error: ${body.response ?? JSON.stringify(body)}`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
474
692
|
|
|
475
693
|
const remote = await fetchActiveAgent(eoaAddress, isTestnet);
|
|
476
694
|
const validUntil =
|
|
@@ -502,6 +720,7 @@ export function HypurrConnectProvider({
|
|
|
502
720
|
setEoaAddress(null);
|
|
503
721
|
setAgent(null);
|
|
504
722
|
setEoaError(null);
|
|
723
|
+
eoaSignerRef.current = null;
|
|
505
724
|
localStorage.removeItem(TELEGRAM_STORAGE_KEY);
|
|
506
725
|
}, []);
|
|
507
726
|
|
|
@@ -543,6 +762,8 @@ export function HypurrConnectProvider({
|
|
|
543
762
|
clearAgent: handleClearAgent,
|
|
544
763
|
|
|
545
764
|
botId: config.telegram?.botId ?? "",
|
|
765
|
+
botUsername: config.telegram?.botUsername ?? "",
|
|
766
|
+
useWidget: config.telegram?.useWidget ?? false,
|
|
546
767
|
|
|
547
768
|
authDataMap,
|
|
548
769
|
telegramClient: tgClient,
|
|
@@ -578,6 +799,8 @@ export function HypurrConnectProvider({
|
|
|
578
799
|
agentReady,
|
|
579
800
|
handleClearAgent,
|
|
580
801
|
config.telegram?.botId,
|
|
802
|
+
config.telegram?.botUsername,
|
|
803
|
+
config.telegram?.useWidget,
|
|
581
804
|
authDataMap,
|
|
582
805
|
tgClient,
|
|
583
806
|
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,
|
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,
|