@livo-build/runtime 0.2.3 → 0.2.5
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 +21 -1
- package/dist/eip712.d.ts +45 -0
- package/dist/eip712.js +160 -0
- package/dist/eip712.test.d.ts +1 -0
- package/dist/eip712.test.js +106 -0
- package/dist/hyperliquid.d.ts +360 -0
- package/dist/hyperliquid.js +350 -0
- package/dist/hyperliquid.test.d.ts +1 -0
- package/dist/hyperliquid.test.js +31 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +14 -0
- package/dist/polymarket.d.ts +146 -0
- package/dist/polymarket.js +495 -0
- package/dist/polymarket.test.d.ts +1 -0
- package/dist/polymarket.test.js +111 -0
- package/dist/sig.d.ts +27 -0
- package/dist/sig.js +50 -0
- package/dist/telegram.d.ts +8 -0
- package/dist/telegram.js +20 -0
- package/dist/watcher.d.ts +83 -0
- package/dist/watcher.js +155 -0
- package/package.json +2 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// Hyperliquid — perps/spot trading + market data, batteries-included for bots.
|
|
2
|
+
//
|
|
3
|
+
// Wraps the well-maintained @nktkas/hyperliquid SDK (Worker-safe, self-contained
|
|
4
|
+
// — bundles its own msgpack) and signs with the runtime's own EIP-712 signer via
|
|
5
|
+
// a viem-account-shaped adapter (localAccount), so NO viem/ethers in the bundle.
|
|
6
|
+
// Reads (prices, books, positions) need no key; trading reads HYPERLIQUID_PRIVATE_KEY
|
|
7
|
+
// (falling back to the bot's managed RELAYER_PRIVATE_KEY) and is opt-in by virtue
|
|
8
|
+
// of a key being present. Mainnet by default; set HYPERLIQUID_TESTNET=1 for testnet.
|
|
9
|
+
//
|
|
10
|
+
// import { Hyperliquid } from "@livo-build/runtime";
|
|
11
|
+
// const hl = new Hyperliquid(env);
|
|
12
|
+
// await hl.mid("BTC"); // crypto perp price, no key
|
|
13
|
+
// await hl.mid("xyz:TSLA"); // equity perp price (builder dex), no key
|
|
14
|
+
// await hl.marketBuy("BTC", 0.001); // needs a funded key
|
|
15
|
+
//
|
|
16
|
+
// IMPORTANT — equities/commodities/FX (TSLA, AAPL, NVDA, GOLD, BRENTOIL, EUR…) are
|
|
17
|
+
// NOT on the main perp dex; they live on Hyperliquid's HIP-3 BUILDER-DEPLOYED perp
|
|
18
|
+
// dexes, addressed as "<dex>:<symbol>" (e.g. "xyz:TSLA"). A bare allMids/l2Book on
|
|
19
|
+
// the main dex returns null for them. This helper resolves builder-dex names for you
|
|
20
|
+
// (mid/book/orders accept "xyz:TSLA"); `findAsset("TSLA")` discovers the full name and
|
|
21
|
+
// `dexs()` lists the deployed dexes. `hl.info` / `hl.exchange` expose the raw clients.
|
|
22
|
+
import { ExchangeClient, HttpTransport, InfoClient } from "@nktkas/hyperliquid";
|
|
23
|
+
import { SymbolConverter } from "@nktkas/hyperliquid/utils";
|
|
24
|
+
import { keccak_256 } from "@noble/hashes/sha3";
|
|
25
|
+
import { localAccount } from "./eip712.js";
|
|
26
|
+
import { privateKeyToAddress } from "./tx.js";
|
|
27
|
+
import { bytesToHex, concatBytes, hexToBytes } from "./hex.js";
|
|
28
|
+
function truthy(v) {
|
|
29
|
+
return v === true || v === 1 || v === "1" || v === "true";
|
|
30
|
+
}
|
|
31
|
+
// Round to ≤5 significant figures — Hyperliquid's price precision rule.
|
|
32
|
+
function formatPrice(px) {
|
|
33
|
+
if (px === 0)
|
|
34
|
+
return "0";
|
|
35
|
+
return String(Number(px.toPrecision(5)));
|
|
36
|
+
}
|
|
37
|
+
function formatSize(size, szDecimals) {
|
|
38
|
+
return size.toFixed(szDecimals);
|
|
39
|
+
}
|
|
40
|
+
// The builder-dex prefix of a coin name ("xyz:TSLA" → "xyz"), or undefined for a
|
|
41
|
+
// main-dex perp / spot symbol.
|
|
42
|
+
function dexOf(coin) {
|
|
43
|
+
const i = coin.indexOf(":");
|
|
44
|
+
return i > 0 ? coin.slice(0, i) : undefined;
|
|
45
|
+
}
|
|
46
|
+
export class Hyperliquid {
|
|
47
|
+
/** True when pointed at testnet. */
|
|
48
|
+
testnet;
|
|
49
|
+
/** The signer's address (null when no key is configured → read-only). */
|
|
50
|
+
address;
|
|
51
|
+
env;
|
|
52
|
+
transport;
|
|
53
|
+
_info;
|
|
54
|
+
privateKey;
|
|
55
|
+
dexOption;
|
|
56
|
+
agentSecret;
|
|
57
|
+
// When set (via forUser), reads default to this account instead of the signer —
|
|
58
|
+
// because in agent/delegation mode the signer is the agent, not the account owner.
|
|
59
|
+
readAddressOverride;
|
|
60
|
+
_exchange;
|
|
61
|
+
_converter;
|
|
62
|
+
_meta;
|
|
63
|
+
constructor(env, options = {}) {
|
|
64
|
+
this.env = env;
|
|
65
|
+
this.testnet = options.testnet ?? truthy(env?.HYPERLIQUID_TESTNET);
|
|
66
|
+
this.transport = new HttpTransport({ isTestnet: this.testnet });
|
|
67
|
+
this._info = new InfoClient({ transport: this.transport });
|
|
68
|
+
this.dexOption = options.dexs ?? true;
|
|
69
|
+
const keySecret = options.privateKeySecret ?? "HYPERLIQUID_PRIVATE_KEY";
|
|
70
|
+
this.privateKey =
|
|
71
|
+
options.privateKey ?? env?.[keySecret] ?? env?.RELAYER_PRIVATE_KEY;
|
|
72
|
+
this.address = this.privateKey ? localAccount(this.privateKey).address : null;
|
|
73
|
+
this.agentSecret =
|
|
74
|
+
options.agentSecret ?? env?.HL_AGENT_SECRET ?? env?.RELAYER_PRIVATE_KEY;
|
|
75
|
+
}
|
|
76
|
+
/** Raw @nktkas InfoClient (all read endpoints). */
|
|
77
|
+
get info() {
|
|
78
|
+
return this._info;
|
|
79
|
+
}
|
|
80
|
+
/** Raw @nktkas ExchangeClient (all signed endpoints). Throws if no key is set. */
|
|
81
|
+
get exchange() {
|
|
82
|
+
if (!this._exchange) {
|
|
83
|
+
if (!this.privateKey) {
|
|
84
|
+
throw new Error("Hyperliquid: trading needs a signing key — set the HYPERLIQUID_PRIVATE_KEY secret (or rely on the bot's RELAYER_PRIVATE_KEY).");
|
|
85
|
+
}
|
|
86
|
+
this._exchange = new ExchangeClient({ transport: this.transport, wallet: localAccount(this.privateKey) });
|
|
87
|
+
}
|
|
88
|
+
return this._exchange;
|
|
89
|
+
}
|
|
90
|
+
// ---- market data (no key needed) ----
|
|
91
|
+
/**
|
|
92
|
+
* All mid prices for a dex, keyed by coin. With no arg → the main perp dex
|
|
93
|
+
* (e.g. { BTC: "95000.0" }). Pass a builder-dex name for equities/commodities
|
|
94
|
+
* (e.g. mids("xyz") → { "xyz:TSLA": "399.8", "xyz:GOLD": "4160.3", … }).
|
|
95
|
+
*/
|
|
96
|
+
async mids(dex) {
|
|
97
|
+
return (await this._info.allMids(dex ? { dex } : undefined));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Mid price of one coin as a number. Accepts a main-dex perp ("BTC") or a
|
|
101
|
+
* builder-dex asset ("xyz:TSLA") — the right dex is queried automatically.
|
|
102
|
+
* Throws if the coin isn't listed (try `findAsset` to resolve a bare ticker).
|
|
103
|
+
*/
|
|
104
|
+
async mid(coin) {
|
|
105
|
+
const mids = await this.mids(dexOf(coin));
|
|
106
|
+
const px = mids[coin];
|
|
107
|
+
if (px === undefined) {
|
|
108
|
+
throw new Error(`Hyperliquid: "${coin}" not found${dexOf(coin) ? "" : ' — equities/commodities live on a builder dex, e.g. "xyz:TSLA"; use findAsset("TSLA") to resolve'}`);
|
|
109
|
+
}
|
|
110
|
+
return Number(px);
|
|
111
|
+
}
|
|
112
|
+
/** L2 order book for a coin (main-dex "BTC" or builder-dex "xyz:TSLA"). */
|
|
113
|
+
async book(coin) {
|
|
114
|
+
return this._info.l2Book({ coin });
|
|
115
|
+
}
|
|
116
|
+
/** Perp account state (positions, margin, withdrawable) for an address (default: the signer). */
|
|
117
|
+
async positions(user) {
|
|
118
|
+
return this._info.clearinghouseState({ user: this.requireUser(user) });
|
|
119
|
+
}
|
|
120
|
+
/** Resting open orders for an address (default: the signer). */
|
|
121
|
+
async openOrders(user) {
|
|
122
|
+
return this._info.openOrders({ user: this.requireUser(user) });
|
|
123
|
+
}
|
|
124
|
+
/** Main perp-dex metadata (universe of coins + szDecimals + maxLeverage). Cached. */
|
|
125
|
+
async meta() {
|
|
126
|
+
if (!this._meta)
|
|
127
|
+
this._meta = this._info.meta();
|
|
128
|
+
return this._meta;
|
|
129
|
+
}
|
|
130
|
+
/** The HIP-3 builder-deployed perp dexes (name + fullName + deployer). */
|
|
131
|
+
async dexs() {
|
|
132
|
+
const list = (await this._info.perpDexs());
|
|
133
|
+
return list.filter((d) => Boolean(d && d.name));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolve a bare ticker to its full Hyperliquid coin name. "BTC" → "BTC" (main
|
|
137
|
+
* dex); "TSLA" → "xyz:TSLA" (a builder dex). Returns null if nothing matches.
|
|
138
|
+
* Use the result with mid/book/limit/market/etc.
|
|
139
|
+
*/
|
|
140
|
+
async findAsset(symbol) {
|
|
141
|
+
if (symbol.includes(":"))
|
|
142
|
+
return symbol; // already dex-qualified (e.g. "xyz:TSLA")
|
|
143
|
+
const want = symbol.toUpperCase();
|
|
144
|
+
const { universe } = await this.meta();
|
|
145
|
+
const main = universe.find((u) => u.name.toUpperCase() === want);
|
|
146
|
+
if (main)
|
|
147
|
+
return main.name;
|
|
148
|
+
for (const d of await this.dexs()) {
|
|
149
|
+
try {
|
|
150
|
+
const m = (await this._info.meta({ dex: d.name }));
|
|
151
|
+
const hit = m.universe.find((u) => u.name.toUpperCase() === `${d.name}:${want}`.toUpperCase());
|
|
152
|
+
if (hit)
|
|
153
|
+
return hit.name;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
/* skip a dex that fails to load */
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
/** The integer asset id Hyperliquid uses in orders for a coin (main, spot, or builder dex). */
|
|
162
|
+
async assetId(coin) {
|
|
163
|
+
return (await this.lookup(coin)).id;
|
|
164
|
+
}
|
|
165
|
+
/** Perp account summary (value, withdrawable, margin) for an address (default: the signer). */
|
|
166
|
+
async balance(user) {
|
|
167
|
+
const s = (await this.positions(user));
|
|
168
|
+
const m = s.marginSummary ?? {};
|
|
169
|
+
return {
|
|
170
|
+
accountValue: Number(m.accountValue ?? 0),
|
|
171
|
+
withdrawable: Number(s.withdrawable ?? 0),
|
|
172
|
+
totalMarginUsed: Number(m.totalMarginUsed ?? 0),
|
|
173
|
+
totalNtlPos: Number(m.totalNtlPos ?? 0),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Spot token balances for an address (default: the signer). */
|
|
177
|
+
async spotBalances(user) {
|
|
178
|
+
return this._info.spotClearinghouseState({ user: this.requireUser(user) });
|
|
179
|
+
}
|
|
180
|
+
/** Recent fills (trade history) for an address (default: the signer). */
|
|
181
|
+
async fills(user) {
|
|
182
|
+
return this._info.userFills({ user: this.requireUser(user) });
|
|
183
|
+
}
|
|
184
|
+
/** Live context for a coin — mark/mid/oracle price, funding rate, open interest, 24h volume. */
|
|
185
|
+
async assetCtx(coin) {
|
|
186
|
+
const dex = dexOf(coin);
|
|
187
|
+
const [meta, ctxs] = (await this._info.metaAndAssetCtxs(dex ? { dex } : undefined));
|
|
188
|
+
const i = meta.universe.findIndex((u) => u.name === coin);
|
|
189
|
+
if (i < 0)
|
|
190
|
+
throw new Error(`Hyperliquid: "${coin}" not found`);
|
|
191
|
+
const c = ctxs[i];
|
|
192
|
+
return {
|
|
193
|
+
coin,
|
|
194
|
+
markPx: Number(c.markPx),
|
|
195
|
+
midPx: c.midPx == null ? null : Number(c.midPx),
|
|
196
|
+
oraclePx: Number(c.oraclePx),
|
|
197
|
+
funding: Number(c.funding),
|
|
198
|
+
openInterest: Number(c.openInterest),
|
|
199
|
+
prevDayPx: Number(c.prevDayPx),
|
|
200
|
+
dayNtlVlm: Number(c.dayNtlVlm),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** OHLCV candles for charting. `interval` default "1h"; window defaults to the last 24h. */
|
|
204
|
+
async candles(coin, interval = "1h", opts = {}) {
|
|
205
|
+
const endTime = opts.endTime ?? Date.now();
|
|
206
|
+
const startTime = opts.startTime ?? endTime - (opts.lookbackMs ?? 24 * 60 * 60 * 1000);
|
|
207
|
+
return this._info.candleSnapshot({ coin, interval, startTime, endTime });
|
|
208
|
+
}
|
|
209
|
+
// ---- trading (key required) ----
|
|
210
|
+
/** Raw order passthrough (the @nktkas `order` shape) for full control. */
|
|
211
|
+
async order(params) {
|
|
212
|
+
return this.exchange.order(params);
|
|
213
|
+
}
|
|
214
|
+
/** Place a limit order. `price`/`size` are numbers; we format to HL's precision. */
|
|
215
|
+
async limit(coin, isBuy, size, price, opts = {}) {
|
|
216
|
+
const { id, szDecimals } = await this.lookup(coin);
|
|
217
|
+
return this.exchange.order({
|
|
218
|
+
orders: [
|
|
219
|
+
{
|
|
220
|
+
a: id,
|
|
221
|
+
b: isBuy,
|
|
222
|
+
p: formatPrice(price),
|
|
223
|
+
s: formatSize(size, szDecimals),
|
|
224
|
+
r: opts.reduceOnly ?? false,
|
|
225
|
+
t: { limit: { tif: opts.tif ?? "Gtc" } },
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
grouping: "na",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
limitBuy(coin, size, price, opts) {
|
|
232
|
+
return this.limit(coin, true, size, price, opts);
|
|
233
|
+
}
|
|
234
|
+
limitSell(coin, size, price, opts) {
|
|
235
|
+
return this.limit(coin, false, size, price, opts);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Market order — an IOC limit priced `slippage` past the current mid so it
|
|
239
|
+
* crosses immediately. Returns the @nktkas order response.
|
|
240
|
+
*/
|
|
241
|
+
async market(coin, isBuy, size, opts = {}) {
|
|
242
|
+
const slippage = opts.slippage ?? 0.05;
|
|
243
|
+
const mid = await this.mid(coin);
|
|
244
|
+
const price = isBuy ? mid * (1 + slippage) : mid * (1 - slippage);
|
|
245
|
+
const { id, szDecimals } = await this.lookup(coin);
|
|
246
|
+
return this.exchange.order({
|
|
247
|
+
orders: [
|
|
248
|
+
{
|
|
249
|
+
a: id,
|
|
250
|
+
b: isBuy,
|
|
251
|
+
p: formatPrice(price),
|
|
252
|
+
s: formatSize(size, szDecimals),
|
|
253
|
+
r: opts.reduceOnly ?? false,
|
|
254
|
+
t: { limit: { tif: "Ioc" } },
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
grouping: "na",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
marketBuy(coin, size, opts) {
|
|
261
|
+
return this.market(coin, true, size, opts);
|
|
262
|
+
}
|
|
263
|
+
marketSell(coin, size, opts) {
|
|
264
|
+
return this.market(coin, false, size, opts);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Market-close the entire open position in `coin` (reduce-only). Reads the
|
|
268
|
+
* current size and crosses the opposite side. No-op (returns closed:false) if flat.
|
|
269
|
+
*/
|
|
270
|
+
async closePosition(coin, opts = {}) {
|
|
271
|
+
const state = (await this.positions());
|
|
272
|
+
const pos = (state.assetPositions ?? []).map((p) => p.position).find((p) => p.coin === coin);
|
|
273
|
+
const szi = pos ? Number(pos.szi) : 0;
|
|
274
|
+
if (szi === 0)
|
|
275
|
+
return { closed: false };
|
|
276
|
+
// Long (szi > 0) → sell to close; short (szi < 0) → buy to close.
|
|
277
|
+
const result = await this.market(coin, szi < 0, Math.abs(szi), { reduceOnly: true, slippage: opts.slippage });
|
|
278
|
+
return { closed: true, side: szi < 0 ? "buy" : "sell", size: Math.abs(szi), result };
|
|
279
|
+
}
|
|
280
|
+
/** Cancel a resting order by its order id (oid). */
|
|
281
|
+
async cancel(coin, oid) {
|
|
282
|
+
return this.exchange.cancel({ cancels: [{ a: await this.assetId(coin), o: oid }] });
|
|
283
|
+
}
|
|
284
|
+
/** Set leverage for a coin (cross by default). */
|
|
285
|
+
async updateLeverage(coin, leverage, opts = {}) {
|
|
286
|
+
return this.exchange.updateLeverage({ asset: await this.assetId(coin), isCross: opts.cross ?? true, leverage });
|
|
287
|
+
}
|
|
288
|
+
// The @nktkas SymbolConverter resolves asset ids + szDecimals across the main
|
|
289
|
+
// perp dex, spot, AND HIP-3 builder dexes (so "xyz:TSLA" → its global asset id).
|
|
290
|
+
symbols() {
|
|
291
|
+
if (!this._converter) {
|
|
292
|
+
this._converter = SymbolConverter.create({ transport: this.transport, dexs: this.dexOption });
|
|
293
|
+
}
|
|
294
|
+
return this._converter;
|
|
295
|
+
}
|
|
296
|
+
async lookup(coin) {
|
|
297
|
+
const c = await this.symbols();
|
|
298
|
+
const id = c.getAssetId(coin);
|
|
299
|
+
const szDecimals = c.getSzDecimals(coin);
|
|
300
|
+
if (id === undefined || szDecimals === undefined) {
|
|
301
|
+
throw new Error(`Hyperliquid: "${coin}" not found${dexOf(coin) ? "" : ' — equities/commodities are on a builder dex, e.g. "xyz:TSLA"; use findAsset("TSLA") to resolve'}`);
|
|
302
|
+
}
|
|
303
|
+
return { id, szDecimals };
|
|
304
|
+
}
|
|
305
|
+
requireUser(user) {
|
|
306
|
+
// In agent/delegation mode (forUser) reads target the account owner, not the agent.
|
|
307
|
+
const who = (user ?? this.readAddressOverride ?? this.address);
|
|
308
|
+
if (!who)
|
|
309
|
+
throw new Error("Hyperliquid: no address — pass one, or configure a signing key.");
|
|
310
|
+
return who;
|
|
311
|
+
}
|
|
312
|
+
// ---- delegation: agent (API) wallets ----
|
|
313
|
+
//
|
|
314
|
+
// Hyperliquid agent/API wallets let a user authorize another key to TRADE on
|
|
315
|
+
// their account WITHOUT being able to withdraw. The flow ("connect & let the app
|
|
316
|
+
// act"): (1) the app derives a per-user agent key here (deterministic, never
|
|
317
|
+
// stored); (2) the USER approves `agentAddress(userId)` once with their OWN wallet
|
|
318
|
+
// via `approveAgent` (in the frontend with their connected wallet — see
|
|
319
|
+
// livo://skill/hyperliquid); (3) the backend trades for them with `forUser(...)`.
|
|
320
|
+
// The agent can place/cancel orders; it can never withdraw their funds.
|
|
321
|
+
/** The per-user agent PRIVATE key (deterministic from the agent secret + userId; never stored). */
|
|
322
|
+
agentKey(userId) {
|
|
323
|
+
if (!this.agentSecret) {
|
|
324
|
+
throw new Error("Hyperliquid: no agent secret — set HL_AGENT_SECRET (or rely on the bot's RELAYER_PRIVATE_KEY) to derive per-user agent wallets.");
|
|
325
|
+
}
|
|
326
|
+
const secretBytes = hexToBytes(this.agentSecret.trim().replace(/^0x/, ""));
|
|
327
|
+
const seed = concatBytes(secretBytes, new TextEncoder().encode("livo-hl-agent:" + userId));
|
|
328
|
+
return bytesToHex(keccak_256(seed)); // 32 bytes → a valid secp256k1 key with overwhelming probability
|
|
329
|
+
}
|
|
330
|
+
/** The agent ADDRESS the user must approve (show this in your connect/approve UI). */
|
|
331
|
+
agentAddress(userId) {
|
|
332
|
+
return privateKeyToAddress(this.agentKey(userId));
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* A Hyperliquid bound to one end-user: it SIGNS orders with that user's agent key
|
|
336
|
+
* and READS/attributes to their `master` account. Use after the user has approved
|
|
337
|
+
* `agentAddress(userId)`. Trading methods (marketBuy/limit/closePosition/cancel/…)
|
|
338
|
+
* then act on the user's account; positions()/balance() default to `master`.
|
|
339
|
+
*/
|
|
340
|
+
forUser(userId, master) {
|
|
341
|
+
const bound = new Hyperliquid(this.env, {
|
|
342
|
+
privateKey: this.agentKey(userId),
|
|
343
|
+
testnet: this.testnet,
|
|
344
|
+
dexs: this.dexOption,
|
|
345
|
+
agentSecret: this.agentSecret,
|
|
346
|
+
});
|
|
347
|
+
bound.readAddressOverride = master;
|
|
348
|
+
return bound;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Hyperliquid agent-wallet delegation: per-user agent keys must be deterministic
|
|
2
|
+
// (recomputable, never stored) and the bound client must sign as the agent.
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { Hyperliquid } from "./hyperliquid.js";
|
|
5
|
+
import { localAccount } from "./eip712.js";
|
|
6
|
+
const SECRET = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
|
|
7
|
+
const MASTER = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826";
|
|
8
|
+
describe("Hyperliquid agent-wallet delegation", () => {
|
|
9
|
+
const hl = new Hyperliquid({ HL_AGENT_SECRET: SECRET });
|
|
10
|
+
it("derives a deterministic per-user agent key", () => {
|
|
11
|
+
expect(hl.agentKey("user-1")).toBe(hl.agentKey("user-1")); // stable
|
|
12
|
+
expect(hl.agentKey("user-1")).not.toBe(hl.agentKey("user-2")); // per-user
|
|
13
|
+
expect(hl.agentKey("user-1")).toMatch(/^0x[0-9a-f]{64}$/);
|
|
14
|
+
});
|
|
15
|
+
it("agentAddress matches the derived key's address", () => {
|
|
16
|
+
expect(hl.agentAddress("user-1").toLowerCase()).toBe(localAccount(hl.agentKey("user-1")).address.toLowerCase());
|
|
17
|
+
});
|
|
18
|
+
it("forUser signs as the agent (not the master)", () => {
|
|
19
|
+
const bound = hl.forUser("user-1", MASTER);
|
|
20
|
+
expect(bound.address?.toLowerCase()).toBe(hl.agentAddress("user-1").toLowerCase());
|
|
21
|
+
expect(bound.address?.toLowerCase()).not.toBe(MASTER.toLowerCase());
|
|
22
|
+
});
|
|
23
|
+
it("changing the agent secret changes every agent address (re-approval needed)", () => {
|
|
24
|
+
const other = new Hyperliquid({}, { agentSecret: "0x" + "11".repeat(32) });
|
|
25
|
+
expect(other.agentAddress("user-1")).not.toBe(hl.agentAddress("user-1"));
|
|
26
|
+
});
|
|
27
|
+
it("throws a clear error when no agent secret is configured", () => {
|
|
28
|
+
const bare = new Hyperliquid({}, { agentSecret: undefined });
|
|
29
|
+
expect(() => bare.agentKey("user-1")).toThrow(/agent secret/i);
|
|
30
|
+
});
|
|
31
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { multicall, MULTICALL3_ADDRESS } from "./multicall.js";
|
|
|
4
4
|
export type { MulticallCall, MulticallOptions } from "./multicall.js";
|
|
5
5
|
export { watchLogs } from "./watch.js";
|
|
6
6
|
export type { WatchLogsOptions, WatchLogsResult } from "./watch.js";
|
|
7
|
+
export { defineWatcher } from "./watcher.js";
|
|
8
|
+
export type { WatcherEnv, WatcherMatch, WatcherScope, WatcherContext, WatcherDefinition, WatcherModule, SubscribeOptions, Confidence, MatchStatus, } from "./watcher.js";
|
|
7
9
|
export { Relayer } from "./relayer.js";
|
|
8
10
|
export { Queue, queue, queueEnvKey } from "./queue.js";
|
|
9
11
|
export type { RelayerOptions } from "./relayer.js";
|
|
@@ -13,6 +15,12 @@ export { Indexer, indexer, indexerEnvKey, GraphQLError } from "./indexer.js";
|
|
|
13
15
|
export type { IndexerOptions } from "./indexer.js";
|
|
14
16
|
export { Telegram } from "./telegram.js";
|
|
15
17
|
export type { TelegramOptions, BotMessage, SendMessageOptions } from "./telegram.js";
|
|
18
|
+
export { Hyperliquid } from "./hyperliquid.js";
|
|
19
|
+
export type { HyperliquidOptions, PlaceOrderOptions, MarketOrderOptions, Tif, CandleInterval, CandlesOptions, AssetContext, AccountBalance, } from "./hyperliquid.js";
|
|
20
|
+
export { Polymarket } from "./polymarket.js";
|
|
21
|
+
export type { PolymarketOptions, PolymarketCreds, PlaceOrderParams, Side, OrderType, SignatureType, PriceInterval, PriceHistoryOptions, MarketOutcome, ResolvedMarket, } from "./polymarket.js";
|
|
22
|
+
export { hashTypedData, signTypedData, localAccount } from "./eip712.js";
|
|
23
|
+
export type { TypedDataDomain, TypedDataField, TypedDataTypes, TypedDataDefinition, ViemTypedDataParams, LocalAccount, } from "./eip712.js";
|
|
16
24
|
export { Store, STORE_TABLE } from "./store.js";
|
|
17
25
|
export type { D1Like } from "./store.js";
|
|
18
26
|
export { log } from "./log.js";
|
|
@@ -23,6 +31,8 @@ export { encodeFunctionData, encodeParameters, decodeParameters, decodeFunctionR
|
|
|
23
31
|
export type { AbiFunction, AbiParameter, ParsedSignature } from "./abi.js";
|
|
24
32
|
export { signTransaction, transactionHashToSign, transactionHash, privateKeyToAddress, toChecksumAddress, } from "./tx.js";
|
|
25
33
|
export type { Eip1559Tx } from "./tx.js";
|
|
34
|
+
export { hashMessage, recoverMessageAddress, verifyMessage } from "./sig.js";
|
|
35
|
+
export type { VerifyMessageParams } from "./sig.js";
|
|
26
36
|
export { eventTopic, encodeFilterTopics, encodeIndexedTopic, decodeLog, parseEventSignature, } from "./events.js";
|
|
27
37
|
export type { AbiEvent, RawLog, DecodedLog } from "./events.js";
|
|
28
38
|
export { bytesToHex, hexToBytes, concatBytes, toBigInt, } from "./hex.js";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ export { Chain, isNonceCollision } from "./chain.js";
|
|
|
7
7
|
export { multicall, MULTICALL3_ADDRESS } from "./multicall.js";
|
|
8
8
|
// watchLogs — resumable, cursor-persisted event catch-up for keepers.
|
|
9
9
|
export { watchLogs } from "./watch.js";
|
|
10
|
+
// defineWatcher — onMatch primitive for Signal Radar watchers (HTTP-triggered,
|
|
11
|
+
// one subscription-addressed match per request) + ctx.subscribe/cancel.
|
|
12
|
+
export { defineWatcher } from "./watcher.js";
|
|
10
13
|
// Relayer — managed signing for bots (custodied RELAYER_PRIVATE_KEY + optional
|
|
11
14
|
// Convex-serialized nonces). A Chain that defaults to the relayer key.
|
|
12
15
|
export { Relayer } from "./relayer.js";
|
|
@@ -20,6 +23,14 @@ export { Wallet, UserWallet } from "./wallet.js";
|
|
|
20
23
|
export { Indexer, indexer, indexerEnvKey, GraphQLError } from "./indexer.js";
|
|
21
24
|
// Telegram — webhook verification + reply plumbing for bots.
|
|
22
25
|
export { Telegram } from "./telegram.js";
|
|
26
|
+
// Hyperliquid — perps/spot data + trading (wraps @nktkas/hyperliquid; signs with
|
|
27
|
+
// the runtime's EIP-712 signer, no viem/ethers in the bundle).
|
|
28
|
+
export { Hyperliquid } from "./hyperliquid.js";
|
|
29
|
+
// Polymarket — prediction-market data (public, frontend-safe) + CLOB order
|
|
30
|
+
// placement (native EIP-712 + L2 HMAC; no @polymarket/clob-client, no ethers).
|
|
31
|
+
export { Polymarket } from "./polymarket.js";
|
|
32
|
+
// EIP-712 — typed-data hashing/signing + a viem-account-shaped local signer.
|
|
33
|
+
export { hashTypedData, signTypedData, localAccount } from "./eip712.js";
|
|
23
34
|
// Layer 3 — State + utilities.
|
|
24
35
|
export { Store, STORE_TABLE } from "./store.js";
|
|
25
36
|
export { log } from "./log.js";
|
|
@@ -28,6 +39,9 @@ export { requireSecret, requireSecrets, getSecret } from "./secret.js";
|
|
|
28
39
|
// Low-level escape hatches — ABI coding, signing, RLP, hex. Power users only.
|
|
29
40
|
export { encodeFunctionData, encodeParameters, decodeParameters, decodeFunctionResult, functionSelector, functionSignature, functionFromSignature, parseSignature, findFunction, keccak256, } from "./abi.js";
|
|
30
41
|
export { signTransaction, transactionHashToSign, transactionHash, privateKeyToAddress, toChecksumAddress, } from "./tx.js";
|
|
42
|
+
// EIP-191 message verification — prove a user controls an address from a signed
|
|
43
|
+
// message (wallet-link / SIWE login), recovered server-side with no hand-rolled crypto.
|
|
44
|
+
export { hashMessage, recoverMessageAddress, verifyMessage } from "./sig.js";
|
|
31
45
|
export { eventTopic, encodeFilterTopics, encodeIndexedTopic, decodeLog, parseEventSignature, } from "./events.js";
|
|
32
46
|
export { bytesToHex, hexToBytes, concatBytes, toBigInt, } from "./hex.js";
|
|
33
47
|
// Layer 2 — generated contract bindings live in "@livo/runtime/contracts".
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export type Side = "BUY" | "SELL";
|
|
2
|
+
export type OrderType = "GTC" | "FOK" | "GTD" | "FAK";
|
|
3
|
+
export type PriceInterval = "1m" | "1h" | "6h" | "1d" | "1w" | "max";
|
|
4
|
+
export interface PriceHistoryOptions {
|
|
5
|
+
/** Coarse window. Default "1d". */
|
|
6
|
+
interval?: PriceInterval;
|
|
7
|
+
/** Resolution in minutes per point (e.g. 60). */
|
|
8
|
+
fidelity?: number;
|
|
9
|
+
/** Explicit window (unix seconds) — overrides `interval` if both given. */
|
|
10
|
+
startTs?: number;
|
|
11
|
+
endTs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** One tradable outcome of a market (the thing you pass as `tokenId`). */
|
|
14
|
+
export interface MarketOutcome {
|
|
15
|
+
outcome: string;
|
|
16
|
+
tokenId: string;
|
|
17
|
+
}
|
|
18
|
+
/** A resolved market: its question + the token id for each outcome. */
|
|
19
|
+
export interface ResolvedMarket {
|
|
20
|
+
question: string;
|
|
21
|
+
conditionId?: string;
|
|
22
|
+
slug?: string;
|
|
23
|
+
negRisk: boolean;
|
|
24
|
+
tokens: MarketOutcome[];
|
|
25
|
+
}
|
|
26
|
+
/** 0 = EOA, 1 = Polymarket Proxy, 2 = Gnosis Safe (clob-client SignatureType). */
|
|
27
|
+
export type SignatureType = 0 | 1 | 2;
|
|
28
|
+
export interface PolymarketCreds {
|
|
29
|
+
key: string;
|
|
30
|
+
secret: string;
|
|
31
|
+
passphrase: string;
|
|
32
|
+
}
|
|
33
|
+
export interface PolymarketOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Enable the deprecated V1 CTF Exchange order path (V2 signing isn't implemented
|
|
36
|
+
* here yet — see docs/POLYMARKET-V2-IMPLEMENTATION.md). V1 orders are rejected on
|
|
37
|
+
* production, so this is for testnet/legacy only. Without it, trading throws (data
|
|
38
|
+
* reads always work).
|
|
39
|
+
*/
|
|
40
|
+
allowLegacyV1?: boolean;
|
|
41
|
+
/** Signing key. Default: env.POLYMARKET_PRIVATE_KEY, then env.RELAYER_PRIVATE_KEY. */
|
|
42
|
+
privateKey?: string;
|
|
43
|
+
/** Env var name to read the key from. Default: "POLYMARKET_PRIVATE_KEY". */
|
|
44
|
+
privateKeySecret?: string;
|
|
45
|
+
/** Polygon chain id. Default 137 (mainnet); 80002 = Amoy testnet. */
|
|
46
|
+
chainId?: number;
|
|
47
|
+
/** Pre-provisioned API creds (else POLYMARKET_API_KEY/SECRET/PASSPHRASE, else derived). */
|
|
48
|
+
creds?: PolymarketCreds;
|
|
49
|
+
/** Override the CLOB host. */
|
|
50
|
+
host?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface PlaceOrderParams {
|
|
53
|
+
/** ERC-1155 outcome token id (the "YES"/"NO" asset). */
|
|
54
|
+
tokenId: string;
|
|
55
|
+
side: Side;
|
|
56
|
+
/** Price per share in USDC, 0–1 (e.g. 0.55). */
|
|
57
|
+
price: number;
|
|
58
|
+
/** Number of shares. */
|
|
59
|
+
size: number;
|
|
60
|
+
/** Order type. Default "GTC". */
|
|
61
|
+
orderType?: OrderType;
|
|
62
|
+
/** Fee rate (bps). Default 0. */
|
|
63
|
+
feeRateBps?: number;
|
|
64
|
+
/** Maker nonce. Default 0. */
|
|
65
|
+
nonce?: number;
|
|
66
|
+
/** Unix expiration (GTD). Default 0 (none). */
|
|
67
|
+
expiration?: number;
|
|
68
|
+
/** Funder address holding the USDC (for proxy/safe). Default: the signer. */
|
|
69
|
+
funder?: string;
|
|
70
|
+
/** Signature type. Default 0 (EOA). */
|
|
71
|
+
signatureType?: SignatureType;
|
|
72
|
+
/** Tick size ("0.1"|"0.01"|"0.001"|"0.0001"). Default: fetched from the CLOB. */
|
|
73
|
+
tickSize?: string;
|
|
74
|
+
/** Whether the market is a neg-risk market. Default: fetched from the CLOB. */
|
|
75
|
+
negRisk?: boolean;
|
|
76
|
+
}
|
|
77
|
+
interface MinimalEnv {
|
|
78
|
+
[key: string]: unknown;
|
|
79
|
+
}
|
|
80
|
+
export declare class Polymarket {
|
|
81
|
+
/** Polygon chain id this client signs for. */
|
|
82
|
+
readonly chainId: number;
|
|
83
|
+
/** The signer's address (null when no key is configured → read-only). */
|
|
84
|
+
readonly address: string | null;
|
|
85
|
+
private readonly host;
|
|
86
|
+
private readonly privateKey?;
|
|
87
|
+
private readonly legacyV1;
|
|
88
|
+
private readonly contracts;
|
|
89
|
+
private creds?;
|
|
90
|
+
constructor(env: MinimalEnv | undefined, options?: PolymarketOptions);
|
|
91
|
+
/** Gamma markets — the rich, searchable market catalog. Pass query params (e.g. { active: true, limit: 10 }). */
|
|
92
|
+
gammaMarkets(query?: Record<string, string | number | boolean>): Promise<unknown>;
|
|
93
|
+
/** A single Gamma market by id. */
|
|
94
|
+
gammaMarket(id: string | number): Promise<unknown>;
|
|
95
|
+
/** CLOB markets (paginated; pass a cursor to page). */
|
|
96
|
+
markets(cursor?: string): Promise<unknown>;
|
|
97
|
+
/** Order book for an outcome token. */
|
|
98
|
+
book(tokenId: string): Promise<unknown>;
|
|
99
|
+
/** Best price for a token on a side. */
|
|
100
|
+
price(tokenId: string, side: Side): Promise<number>;
|
|
101
|
+
/** Midpoint price for a token. */
|
|
102
|
+
midpoint(tokenId: string): Promise<number>;
|
|
103
|
+
/** Minimum tick size for a token (e.g. "0.01"). */
|
|
104
|
+
tickSize(tokenId: string): Promise<string>;
|
|
105
|
+
/** Whether a token's market is a neg-risk market. */
|
|
106
|
+
negRisk(tokenId: string): Promise<boolean>;
|
|
107
|
+
/** Last traded price for a token. */
|
|
108
|
+
lastTradePrice(tokenId: string): Promise<number>;
|
|
109
|
+
/** Bid/ask spread for a token. */
|
|
110
|
+
spread(tokenId: string): Promise<unknown>;
|
|
111
|
+
/** Historical price points for charting a token. */
|
|
112
|
+
priceHistory(tokenId: string, opts?: PriceHistoryOptions): Promise<unknown>;
|
|
113
|
+
/**
|
|
114
|
+
* Resolve a market by slug, numeric Gamma id, or 0x condition id → its question
|
|
115
|
+
* and the token id for each outcome. THE way to turn "will X happen?" into the
|
|
116
|
+
* `tokenId` you trade: `(await pm.resolveMarket(slug)).tokens` → [{outcome:"Yes",tokenId},…].
|
|
117
|
+
*/
|
|
118
|
+
resolveMarket(slugOrId: string): Promise<ResolvedMarket | null>;
|
|
119
|
+
/** The collateral token address to fund/approve (pUSD on V2; USDC.e on legacy V1). */
|
|
120
|
+
get collateralToken(): string | undefined;
|
|
121
|
+
/** A user's open positions (holdings) from the public data API (default: the signer). */
|
|
122
|
+
positions(user?: string): Promise<unknown>;
|
|
123
|
+
/** Ensure API credentials exist (derive existing, else create). Cached on the instance. */
|
|
124
|
+
ensureCreds(): Promise<PolymarketCreds>;
|
|
125
|
+
private assertTradingEnabled;
|
|
126
|
+
private deriveOrCreateApiKey;
|
|
127
|
+
/** Build, sign, and submit an order. Returns the CLOB response JSON. */
|
|
128
|
+
placeOrder(params: PlaceOrderParams): Promise<unknown>;
|
|
129
|
+
/** Cancel one order by id. */
|
|
130
|
+
cancelOrder(orderId: string): Promise<unknown>;
|
|
131
|
+
/** Cancel all of the signer's open orders. */
|
|
132
|
+
cancelAll(): Promise<unknown>;
|
|
133
|
+
/** The signer's open orders (L2-authed). */
|
|
134
|
+
openOrders(): Promise<unknown>;
|
|
135
|
+
/**
|
|
136
|
+
* The signer's USDC collateral balance + exchange allowance (or, with a tokenId,
|
|
137
|
+
* the conditional-token balance). Check this before trading — orders fail if the
|
|
138
|
+
* CTF Exchange isn't approved to spend your USDC.e.
|
|
139
|
+
*/
|
|
140
|
+
balances(tokenId?: string): Promise<unknown>;
|
|
141
|
+
private l1Headers;
|
|
142
|
+
private l2Headers;
|
|
143
|
+
private requireSigner;
|
|
144
|
+
private getJson;
|
|
145
|
+
}
|
|
146
|
+
export {};
|