@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
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ExchangeClient,
|
|
3
3
|
HttpTransport,
|
|
4
|
-
|
|
4
|
+
type IRequestTransport,
|
|
5
5
|
} from "@hfunlabs/hyperliquid";
|
|
6
|
-
import { PrivateKeySigner } from "@hfunlabs/hyperliquid/signing";
|
|
6
|
+
import { PrivateKeySigner, signUserSignedAction } from "@hfunlabs/hyperliquid/signing";
|
|
7
7
|
import type { TelegramUserResponse } from "hypurr-grpc/ts/hypurr/telegram/telegram_service";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
TelegramUser as HypurrTelegramUser,
|
|
10
|
+
TelegramChatWalletPack,
|
|
11
|
+
} from "hypurr-grpc/ts/hypurr/user";
|
|
12
|
+
import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
|
|
9
13
|
import {
|
|
10
14
|
createContext,
|
|
11
15
|
useCallback,
|
|
12
16
|
useContext,
|
|
13
17
|
useEffect,
|
|
14
18
|
useMemo,
|
|
19
|
+
useRef,
|
|
15
20
|
useState,
|
|
16
21
|
type ReactNode,
|
|
17
22
|
} from "react";
|
|
18
23
|
import {
|
|
24
|
+
AGENT_NAME,
|
|
19
25
|
clearAgent as clearStoredAgent,
|
|
26
|
+
fetchActiveAgent,
|
|
20
27
|
generateAgentKey,
|
|
28
|
+
isAgentValid,
|
|
29
|
+
isDeadAgentError,
|
|
21
30
|
loadAgent,
|
|
22
31
|
saveAgent,
|
|
23
32
|
} from "./agent";
|
|
@@ -25,6 +34,7 @@ import { createStaticClient, createTelegramClient } from "./grpc";
|
|
|
25
34
|
import { GrpcExchangeTransport } from "./GrpcExchangeTransport";
|
|
26
35
|
import type {
|
|
27
36
|
AuthMethod,
|
|
37
|
+
EoaSigner,
|
|
28
38
|
HypurrConnectConfig,
|
|
29
39
|
HypurrConnectState,
|
|
30
40
|
HypurrUser,
|
|
@@ -33,6 +43,13 @@ import type {
|
|
|
33
43
|
TelegramLoginData,
|
|
34
44
|
} from "./types";
|
|
35
45
|
|
|
46
|
+
/** @internal context value — extends the public type with fields used only by library internals */
|
|
47
|
+
interface InternalConnectState extends HypurrConnectState {
|
|
48
|
+
loginTelegram: (data: TelegramLoginData) => void;
|
|
49
|
+
botUsername: string;
|
|
50
|
+
useWidget: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
const TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-user";
|
|
37
54
|
|
|
38
55
|
function toAuthDataMap(data: TelegramLoginData): Record<string, string> {
|
|
@@ -48,7 +65,7 @@ function toAuthDataMap(data: TelegramLoginData): Record<string, string> {
|
|
|
48
65
|
return map;
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
const HypurrConnectContext = createContext<
|
|
68
|
+
const HypurrConnectContext = createContext<InternalConnectState | null>(null);
|
|
52
69
|
|
|
53
70
|
export function useHypurrConnect(): HypurrConnectState {
|
|
54
71
|
const ctx = useContext(HypurrConnectContext);
|
|
@@ -59,6 +76,16 @@ export function useHypurrConnect(): HypurrConnectState {
|
|
|
59
76
|
return ctx;
|
|
60
77
|
}
|
|
61
78
|
|
|
79
|
+
/** @internal — gives library components access to fields not on the public API */
|
|
80
|
+
export function useHypurrConnectInternal(): InternalConnectState {
|
|
81
|
+
const ctx = useContext(HypurrConnectContext);
|
|
82
|
+
if (!ctx)
|
|
83
|
+
throw new Error(
|
|
84
|
+
"useHypurrConnectInternal must be used within <HypurrConnectProvider>",
|
|
85
|
+
);
|
|
86
|
+
return ctx;
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
export function HypurrConnectProvider({
|
|
63
90
|
config,
|
|
64
91
|
children,
|
|
@@ -89,6 +116,8 @@ export function HypurrConnectProvider({
|
|
|
89
116
|
[tgLoginData],
|
|
90
117
|
);
|
|
91
118
|
|
|
119
|
+
const [tgUserTick, setTgUserTick] = useState(0);
|
|
120
|
+
|
|
92
121
|
useEffect(() => {
|
|
93
122
|
if (!tgLoginData) return;
|
|
94
123
|
let cancelled = false;
|
|
@@ -98,9 +127,7 @@ export function HypurrConnectProvider({
|
|
|
98
127
|
(async () => {
|
|
99
128
|
try {
|
|
100
129
|
const authData = toAuthDataMap(tgLoginData);
|
|
101
|
-
console.log(authData);
|
|
102
130
|
const { response } = await tgClient.telegramUser({ authData });
|
|
103
|
-
console.log(response);
|
|
104
131
|
if (cancelled) return;
|
|
105
132
|
setTgUser((response as TelegramUserResponse).user ?? null);
|
|
106
133
|
} catch (err) {
|
|
@@ -115,19 +142,14 @@ export function HypurrConnectProvider({
|
|
|
115
142
|
return () => {
|
|
116
143
|
cancelled = true;
|
|
117
144
|
};
|
|
118
|
-
}, [tgLoginData, tgClient]);
|
|
145
|
+
}, [tgLoginData, tgClient, tgUserTick]);
|
|
119
146
|
|
|
120
147
|
// ── EOA auth state ───────────────────────────────────────────
|
|
121
148
|
const [eoaAddress, setEoaAddress] = useState<`0x${string}` | null>(null);
|
|
122
149
|
const [agent, setAgent] = useState<StoredAgent | null>(null);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
setAgent(loadAgent(eoaAddress));
|
|
127
|
-
} else {
|
|
128
|
-
setAgent(null);
|
|
129
|
-
}
|
|
130
|
-
}, [eoaAddress]);
|
|
150
|
+
const [eoaLoading, setEoaLoading] = useState(false);
|
|
151
|
+
const [eoaError, setEoaError] = useState<string | null>(null);
|
|
152
|
+
const eoaSignerRef = useRef<EoaSigner | null>(null);
|
|
131
153
|
|
|
132
154
|
// ── Derived auth ─────────────────────────────────────────────
|
|
133
155
|
const authMethod: AuthMethod = tgLoginData
|
|
@@ -136,19 +158,59 @@ export function HypurrConnectProvider({
|
|
|
136
158
|
? "eoa"
|
|
137
159
|
: null;
|
|
138
160
|
|
|
139
|
-
|
|
161
|
+
// ── Multi-wallet state (Telegram) ─────────────────────────────
|
|
162
|
+
const [wallets, setWallets] = useState<HyperliquidWallet[]>([]);
|
|
163
|
+
const [selectedWalletId, setSelectedWalletId] = useState<number>(0);
|
|
164
|
+
const [packs, setPacks] = useState<TelegramChatWalletPack[]>([]);
|
|
165
|
+
|
|
166
|
+
const refreshWallets = useCallback(() => setTgUserTick((t) => t + 1), []);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (authMethod !== "telegram" || !tgUser) {
|
|
170
|
+
setWallets([]);
|
|
171
|
+
setSelectedWalletId(0);
|
|
172
|
+
setPacks([]);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const userWallets = tgUser.wallets ?? [];
|
|
177
|
+
setWallets(userWallets);
|
|
178
|
+
setPacks(tgUser.packs ?? []);
|
|
179
|
+
|
|
180
|
+
const defaultId = tgUser.walletId || userWallets[0]?.id || 0;
|
|
181
|
+
setSelectedWalletId((prev) => {
|
|
182
|
+
if (prev && userWallets.some((w) => w.id === prev)) return prev;
|
|
183
|
+
return defaultId;
|
|
184
|
+
});
|
|
185
|
+
}, [authMethod, tgUser]);
|
|
186
|
+
|
|
187
|
+
const selectedWallet = useMemo(
|
|
188
|
+
() => wallets.find((w) => w.id === selectedWalletId) ?? wallets[0] ?? null,
|
|
189
|
+
[wallets, selectedWalletId],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const selectWallet = useCallback(
|
|
193
|
+
(walletId: number) => {
|
|
194
|
+
if (wallets.some((w) => w.id === walletId)) {
|
|
195
|
+
setSelectedWalletId(walletId);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
[wallets],
|
|
199
|
+
);
|
|
140
200
|
|
|
141
201
|
const user = useMemo<HypurrUser | null>(() => {
|
|
142
|
-
if (tgLoginData && authMethod === "telegram") {
|
|
202
|
+
if (tgLoginData && authMethod === "telegram" && selectedWallet) {
|
|
143
203
|
return {
|
|
144
|
-
address:
|
|
145
|
-
walletId:
|
|
204
|
+
address: selectedWallet.ethereumAddress,
|
|
205
|
+
walletId: selectedWallet.id,
|
|
146
206
|
displayName: tgLoginData.username
|
|
147
207
|
? `@${tgLoginData.username}`
|
|
148
208
|
: tgLoginData.first_name,
|
|
149
209
|
photoUrl: tgLoginData.photo_url,
|
|
150
210
|
authMethod: "telegram",
|
|
151
211
|
telegramId: String(tgLoginData.id),
|
|
212
|
+
hfunScore: tgUser?.reputation?.hfunScore,
|
|
213
|
+
reputationScore: tgUser?.reputation?.reputationScore,
|
|
152
214
|
};
|
|
153
215
|
}
|
|
154
216
|
if (eoaAddress && authMethod === "eoa") {
|
|
@@ -160,11 +222,43 @@ export function HypurrConnectProvider({
|
|
|
160
222
|
};
|
|
161
223
|
}
|
|
162
224
|
return null;
|
|
163
|
-
}, [tgLoginData,
|
|
225
|
+
}, [tgLoginData, selectedWallet, eoaAddress, authMethod, tgUser]);
|
|
164
226
|
|
|
165
227
|
// ── Exchange client ──────────────────────────────────────────
|
|
166
228
|
// Telegram: GrpcExchangeTransport → HyperliquidCoreAction (server signs)
|
|
167
|
-
// 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).
|
|
235
|
+
|
|
236
|
+
const onDeadAgentRef = useRef<((address: `0x${string}`) => void) | null>(
|
|
237
|
+
null,
|
|
238
|
+
);
|
|
239
|
+
onDeadAgentRef.current = (addr: `0x${string}`) => {
|
|
240
|
+
clearStoredAgent(addr);
|
|
241
|
+
setAgent(null);
|
|
242
|
+
setEoaError("Agent expired or was deregistered. Please reconnect.");
|
|
243
|
+
};
|
|
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
|
+
|
|
259
|
+
const agentReady =
|
|
260
|
+
authMethod === "telegram" || (authMethod === "eoa" && !!agent);
|
|
261
|
+
|
|
168
262
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
169
263
|
const exchange = useMemo<ExchangeClient<any> | null>(() => {
|
|
170
264
|
if (authMethod === "telegram" && user?.address) {
|
|
@@ -181,150 +275,304 @@ export function HypurrConnectProvider({
|
|
|
181
275
|
});
|
|
182
276
|
}
|
|
183
277
|
|
|
184
|
-
if (authMethod === "eoa" &&
|
|
185
|
-
const
|
|
186
|
-
return new ExchangeClient({
|
|
187
|
-
transport: new HttpTransport({
|
|
188
|
-
isTestnet: config.isTestnet ?? false,
|
|
189
|
-
}),
|
|
190
|
-
wallet,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return null;
|
|
195
|
-
}, [authMethod, user, agent, config.isTestnet, tgClient, authDataMap]);
|
|
278
|
+
if (authMethod === "eoa" && eoaAddress) {
|
|
279
|
+
const hasSigner = !!eoaSignerRef.current;
|
|
196
280
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
() =>
|
|
200
|
-
new InfoClient({
|
|
201
|
-
transport: new HttpTransport({
|
|
281
|
+
if (!agent && !hasSigner) {
|
|
282
|
+
const noAgentTransport: IRequestTransport = {
|
|
202
283
|
isTestnet: config.isTestnet ?? false,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const addr = user?.address;
|
|
216
|
-
if (!addr) {
|
|
217
|
-
setUsdcBalance(null);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
let cancelled = false;
|
|
222
|
-
setUsdcBalanceLoading(true);
|
|
223
|
-
|
|
224
|
-
(async () => {
|
|
225
|
-
try {
|
|
226
|
-
const state = await infoClient.clearinghouseState({
|
|
227
|
-
user: addr as `0x${string}`,
|
|
284
|
+
request(): Promise<never> {
|
|
285
|
+
throw new Error(
|
|
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 }).",
|
|
289
|
+
);
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
return new ExchangeClient({
|
|
293
|
+
transport: noAgentTransport,
|
|
294
|
+
externalSigning: true,
|
|
295
|
+
userAddress: eoaAddress,
|
|
228
296
|
});
|
|
229
|
-
if (!cancelled) {
|
|
230
|
-
setUsdcBalance(state.withdrawable);
|
|
231
|
-
}
|
|
232
|
-
} catch (err) {
|
|
233
|
-
console.error("[HypurrConnect] Failed to fetch USDC balance:", err);
|
|
234
|
-
if (!cancelled) setUsdcBalance(null);
|
|
235
|
-
} finally {
|
|
236
|
-
if (!cancelled) setUsdcBalanceLoading(false);
|
|
237
297
|
}
|
|
238
|
-
})();
|
|
239
|
-
|
|
240
|
-
return () => {
|
|
241
|
-
cancelled = true;
|
|
242
|
-
};
|
|
243
|
-
}, [user?.address, infoClient, balanceTick]);
|
|
244
|
-
|
|
245
|
-
// ── Agent approval (EOA flow) ────────────────────────────────
|
|
246
|
-
const approveAgent = useCallback(
|
|
247
|
-
async (signTypedDataAsync: SignTypedDataFn) => {
|
|
248
|
-
if (!eoaAddress) throw new Error("No EOA address connected");
|
|
249
|
-
|
|
250
|
-
const { privateKey, address: agentAddress } = await generateAgentKey();
|
|
251
298
|
|
|
252
299
|
const isTestnet = config.isTestnet ?? false;
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
300
|
+
const inner = new HttpTransport({ isTestnet });
|
|
301
|
+
const deadAgentAddr = eoaAddress;
|
|
302
|
+
const guardedTransport: IRequestTransport = {
|
|
303
|
+
isTestnet: inner.isTestnet,
|
|
304
|
+
async request<T>(
|
|
305
|
+
endpoint: "info" | "exchange" | "explorer",
|
|
306
|
+
payload: unknown,
|
|
307
|
+
signal?: AbortSignal,
|
|
308
|
+
): Promise<T> {
|
|
309
|
+
try {
|
|
310
|
+
return await inner.request<T>(endpoint, payload, signal);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (endpoint === "exchange" && isDeadAgentError(err)) {
|
|
313
|
+
onDeadAgentRef.current?.(deadAgentAddr);
|
|
314
|
+
}
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
},
|
|
261
318
|
};
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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;
|
|
269
429
|
};
|
|
270
430
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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);
|
|
277
463
|
},
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
return new ExchangeClient({
|
|
467
|
+
transport: guardedTransport,
|
|
468
|
+
wallet: dualWallet,
|
|
469
|
+
signatureChainId: () => {
|
|
470
|
+
const id = signerRef.current?.chainId ?? 42161;
|
|
471
|
+
return `0x${id.toString(16)}` as `0x${string}`;
|
|
285
472
|
},
|
|
286
473
|
});
|
|
474
|
+
}
|
|
287
475
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
476
|
+
return null;
|
|
477
|
+
}, [
|
|
478
|
+
authMethod,
|
|
479
|
+
user,
|
|
480
|
+
agent,
|
|
481
|
+
eoaAddress,
|
|
482
|
+
config.isTestnet,
|
|
483
|
+
tgClient,
|
|
484
|
+
authDataMap,
|
|
485
|
+
]);
|
|
291
486
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
487
|
+
const handleClearAgent = useCallback(() => {
|
|
488
|
+
if (eoaAddress) {
|
|
489
|
+
clearStoredAgent(eoaAddress);
|
|
490
|
+
setAgent(null);
|
|
491
|
+
}
|
|
492
|
+
}, [eoaAddress]);
|
|
295
493
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
signature: { r, s, v },
|
|
303
|
-
}),
|
|
494
|
+
// ── Wallet management (Telegram only) ───────────────────────
|
|
495
|
+
const createWallet = useCallback(
|
|
496
|
+
async (name: string): Promise<HyperliquidWallet> => {
|
|
497
|
+
const { response } = await tgClient.hyperliquidWalletCreate({
|
|
498
|
+
authData: authDataMap,
|
|
499
|
+
name,
|
|
304
500
|
});
|
|
501
|
+
refreshWallets();
|
|
502
|
+
if (!response.wallet)
|
|
503
|
+
throw new Error("Wallet creation returned no wallet");
|
|
504
|
+
return response.wallet;
|
|
505
|
+
},
|
|
506
|
+
[tgClient, authDataMap, refreshWallets],
|
|
507
|
+
);
|
|
305
508
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
509
|
+
const deleteWallet = useCallback(
|
|
510
|
+
async (walletId: number): Promise<void> => {
|
|
511
|
+
await tgClient.hyperliquidWalletDelete({
|
|
512
|
+
authData: authDataMap,
|
|
513
|
+
walletId,
|
|
514
|
+
});
|
|
515
|
+
if (walletId === selectedWalletId) {
|
|
516
|
+
const remaining = wallets.filter((w) => w.id !== walletId);
|
|
517
|
+
setSelectedWalletId(remaining[0]?.id ?? 0);
|
|
309
518
|
}
|
|
519
|
+
refreshWallets();
|
|
520
|
+
},
|
|
521
|
+
[tgClient, authDataMap, selectedWalletId, wallets, refreshWallets],
|
|
522
|
+
);
|
|
310
523
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
524
|
+
const createWalletPack = useCallback(
|
|
525
|
+
async (name: string): Promise<number> => {
|
|
526
|
+
const { response } = await tgClient.telegramChatWalletPackCreate({
|
|
527
|
+
authData: authDataMap,
|
|
528
|
+
name,
|
|
529
|
+
});
|
|
530
|
+
refreshWallets();
|
|
531
|
+
return response.packId;
|
|
318
532
|
},
|
|
319
|
-
[
|
|
533
|
+
[tgClient, authDataMap, refreshWallets],
|
|
320
534
|
);
|
|
321
535
|
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
536
|
+
const addPackLabel = useCallback(
|
|
537
|
+
async (params: {
|
|
538
|
+
walletAddress: string;
|
|
539
|
+
walletLabel: string;
|
|
540
|
+
packId: number;
|
|
541
|
+
}): Promise<void> => {
|
|
542
|
+
await tgClient.telegramChatWalletPackLabelAdd({
|
|
543
|
+
authData: authDataMap,
|
|
544
|
+
...params,
|
|
545
|
+
});
|
|
546
|
+
refreshWallets();
|
|
547
|
+
},
|
|
548
|
+
[tgClient, authDataMap, refreshWallets],
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const modifyPackLabel = useCallback(
|
|
552
|
+
async (params: {
|
|
553
|
+
walletLabelOld: string;
|
|
554
|
+
walletLabelNew: string;
|
|
555
|
+
packId: number;
|
|
556
|
+
}): Promise<void> => {
|
|
557
|
+
await tgClient.telegramChatWalletPackLabelModify({
|
|
558
|
+
authData: authDataMap,
|
|
559
|
+
...params,
|
|
560
|
+
});
|
|
561
|
+
refreshWallets();
|
|
562
|
+
},
|
|
563
|
+
[tgClient, authDataMap, refreshWallets],
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const removePackLabel = useCallback(
|
|
567
|
+
async (params: { walletLabel: string; packId: number }): Promise<void> => {
|
|
568
|
+
await tgClient.telegramChatWalletPackLabelRemove({
|
|
569
|
+
authData: authDataMap,
|
|
570
|
+
...params,
|
|
571
|
+
});
|
|
572
|
+
refreshWallets();
|
|
573
|
+
},
|
|
574
|
+
[tgClient, authDataMap, refreshWallets],
|
|
575
|
+
);
|
|
328
576
|
|
|
329
577
|
// ── Login modal state ────────────────────────────────────────
|
|
330
578
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
|
@@ -337,15 +585,133 @@ export function HypurrConnectProvider({
|
|
|
337
585
|
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(data));
|
|
338
586
|
setEoaAddress(null);
|
|
339
587
|
setAgent(null);
|
|
588
|
+
setEoaError(null);
|
|
340
589
|
}, []);
|
|
341
590
|
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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);
|
|
600
|
+
|
|
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
|
+
);
|
|
611
|
+
|
|
612
|
+
const approveAgentFn = useCallback(
|
|
613
|
+
async (signTypedDataAsync: SignTypedDataFn, chainId: number) => {
|
|
614
|
+
if (!eoaAddress) {
|
|
615
|
+
throw new Error(
|
|
616
|
+
"[HypurrConnect] Cannot approve agent: no EOA wallet connected. Call connectEoa(address) first.",
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
eoaSignerRef.current = { signTypedData: signTypedDataAsync, chainId };
|
|
621
|
+
|
|
622
|
+
setEoaLoading(true);
|
|
623
|
+
setEoaError(null);
|
|
624
|
+
try {
|
|
625
|
+
const existing = loadAgent(eoaAddress);
|
|
626
|
+
if (existing) {
|
|
627
|
+
const isTestnet = config.isTestnet ?? false;
|
|
628
|
+
const valid = await isAgentValid(existing, eoaAddress, isTestnet);
|
|
629
|
+
if (valid) {
|
|
630
|
+
setAgent(existing);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
clearStoredAgent(eoaAddress);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const { privateKey, address: agentAddress } = await generateAgentKey();
|
|
637
|
+
const isTestnet = config.isTestnet ?? false;
|
|
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
|
+
|
|
661
|
+
const wallet = {
|
|
662
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
663
|
+
signTypedData(params: any) {
|
|
664
|
+
return signTypedDataAsync(params);
|
|
665
|
+
},
|
|
666
|
+
getAddresses: async () => [eoaAddress] as `0x${string}`[],
|
|
667
|
+
getChainId: async () => chainId,
|
|
668
|
+
};
|
|
669
|
+
|
|
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
|
+
}
|
|
692
|
+
|
|
693
|
+
const remote = await fetchActiveAgent(eoaAddress, isTestnet);
|
|
694
|
+
const validUntil =
|
|
695
|
+
remote?.validUntil ?? Date.now() + 7 * 24 * 60 * 60 * 1000;
|
|
696
|
+
|
|
697
|
+
const stored: StoredAgent = {
|
|
698
|
+
privateKey,
|
|
699
|
+
address: agentAddress,
|
|
700
|
+
approvedAt: Date.now(),
|
|
701
|
+
validUntil,
|
|
702
|
+
};
|
|
703
|
+
saveAgent(eoaAddress, stored);
|
|
704
|
+
setAgent(stored);
|
|
705
|
+
} catch (err) {
|
|
706
|
+
console.error("[HypurrConnect] EOA agent approval failed:", err);
|
|
707
|
+
setEoaError(err instanceof Error ? err.message : String(err));
|
|
708
|
+
setAgent(null);
|
|
709
|
+
} finally {
|
|
710
|
+
setEoaLoading(false);
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
[eoaAddress, config.isTestnet],
|
|
714
|
+
);
|
|
349
715
|
|
|
350
716
|
const logout = useCallback(() => {
|
|
351
717
|
setTgLoginData(null);
|
|
@@ -353,37 +719,51 @@ export function HypurrConnectProvider({
|
|
|
353
719
|
setTgError(null);
|
|
354
720
|
setEoaAddress(null);
|
|
355
721
|
setAgent(null);
|
|
722
|
+
setEoaError(null);
|
|
723
|
+
eoaSignerRef.current = null;
|
|
356
724
|
localStorage.removeItem(TELEGRAM_STORAGE_KEY);
|
|
357
725
|
}, []);
|
|
358
726
|
|
|
359
727
|
// ── Context value ────────────────────────────────────────────
|
|
360
|
-
const value = useMemo<
|
|
728
|
+
const value = useMemo<InternalConnectState>(
|
|
361
729
|
() => ({
|
|
362
730
|
user,
|
|
363
731
|
isLoggedIn: !!user,
|
|
364
|
-
isLoading: tgLoading,
|
|
365
|
-
error: tgError,
|
|
732
|
+
isLoading: tgLoading || eoaLoading,
|
|
733
|
+
error: tgError ?? eoaError,
|
|
366
734
|
authMethod,
|
|
367
735
|
exchange,
|
|
368
736
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
737
|
+
wallets,
|
|
738
|
+
selectedWalletId,
|
|
739
|
+
selectWallet,
|
|
740
|
+
|
|
741
|
+
createWallet,
|
|
742
|
+
deleteWallet,
|
|
743
|
+
refreshWallets,
|
|
744
|
+
|
|
745
|
+
packs,
|
|
746
|
+
createWalletPack,
|
|
747
|
+
addPackLabel,
|
|
748
|
+
modifyPackLabel,
|
|
749
|
+
removePackLabel,
|
|
372
750
|
|
|
373
751
|
loginModalOpen,
|
|
374
752
|
openLoginModal,
|
|
375
753
|
closeLoginModal,
|
|
376
754
|
|
|
377
755
|
loginTelegram,
|
|
378
|
-
|
|
756
|
+
connectEoa,
|
|
757
|
+
approveAgent: approveAgentFn,
|
|
379
758
|
logout,
|
|
380
759
|
|
|
381
760
|
agent,
|
|
382
|
-
agentReady
|
|
383
|
-
approveAgent,
|
|
761
|
+
agentReady,
|
|
384
762
|
clearAgent: handleClearAgent,
|
|
385
763
|
|
|
386
764
|
botId: config.telegram?.botId ?? "",
|
|
765
|
+
botUsername: config.telegram?.botUsername ?? "",
|
|
766
|
+
useWidget: config.telegram?.useWidget ?? false,
|
|
387
767
|
|
|
388
768
|
authDataMap,
|
|
389
769
|
telegramClient: tgClient,
|
|
@@ -392,22 +772,35 @@ export function HypurrConnectProvider({
|
|
|
392
772
|
[
|
|
393
773
|
user,
|
|
394
774
|
tgLoading,
|
|
775
|
+
eoaLoading,
|
|
395
776
|
tgError,
|
|
777
|
+
eoaError,
|
|
396
778
|
authMethod,
|
|
397
779
|
exchange,
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
780
|
+
wallets,
|
|
781
|
+
selectedWalletId,
|
|
782
|
+
selectWallet,
|
|
783
|
+
createWallet,
|
|
784
|
+
deleteWallet,
|
|
785
|
+
refreshWallets,
|
|
786
|
+
packs,
|
|
787
|
+
createWalletPack,
|
|
788
|
+
addPackLabel,
|
|
789
|
+
modifyPackLabel,
|
|
790
|
+
removePackLabel,
|
|
401
791
|
loginModalOpen,
|
|
402
792
|
openLoginModal,
|
|
403
793
|
closeLoginModal,
|
|
404
794
|
loginTelegram,
|
|
405
|
-
|
|
795
|
+
connectEoa,
|
|
796
|
+
approveAgentFn,
|
|
406
797
|
logout,
|
|
407
798
|
agent,
|
|
408
|
-
|
|
799
|
+
agentReady,
|
|
409
800
|
handleClearAgent,
|
|
410
801
|
config.telegram?.botId,
|
|
802
|
+
config.telegram?.botUsername,
|
|
803
|
+
config.telegram?.useWidget,
|
|
411
804
|
authDataMap,
|
|
412
805
|
tgClient,
|
|
413
806
|
staticClient,
|