@livo-build/runtime 0.2.6 → 0.2.12
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/dist/aes.d.ts +4 -0
- package/dist/aes.js +38 -0
- package/dist/auth.test.d.ts +1 -0
- package/dist/auth.test.js +57 -0
- package/dist/cache.d.ts +12 -0
- package/dist/cache.js +27 -0
- package/dist/http.d.ts +48 -0
- package/dist/http.js +133 -0
- package/dist/hyperliquid.d.ts +269 -0
- package/dist/hyperliquid.js +194 -0
- package/dist/hyperliquid.test.js +17 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +13 -0
- package/dist/nft.d.ts +33 -0
- package/dist/nft.js +72 -0
- package/dist/nft.test.d.ts +1 -0
- package/dist/nft.test.js +44 -0
- package/dist/ratelimit.d.ts +19 -0
- package/dist/ratelimit.js +11 -0
- package/dist/reads.d.ts +12 -0
- package/dist/reads.js +36 -0
- package/dist/sessions.d.ts +8 -0
- package/dist/sessions.js +60 -0
- package/dist/signals.d.ts +131 -0
- package/dist/signals.js +146 -0
- package/dist/siwe.d.ts +24 -0
- package/dist/siwe.js +33 -0
- package/dist/sse.test.d.ts +1 -0
- package/dist/sse.test.js +28 -0
- package/dist/webhook.d.ts +18 -0
- package/dist/webhook.js +49 -0
- package/dist/webhook.test.d.ts +1 -0
- package/dist/webhook.test.js +46 -0
- package/package.json +1 -1
package/dist/hyperliquid.js
CHANGED
|
@@ -25,6 +25,35 @@ import { keccak_256 } from "@noble/hashes/sha3";
|
|
|
25
25
|
import { localAccount } from "./eip712.js";
|
|
26
26
|
import { privateKeyToAddress } from "./tx.js";
|
|
27
27
|
import { bytesToHex, concatBytes, hexToBytes } from "./hex.js";
|
|
28
|
+
/** Parse a Hyperliquid clearinghouse position into a clean PositionInfo. */
|
|
29
|
+
export function parseHlPosition(p) {
|
|
30
|
+
const szi = Number(p.szi);
|
|
31
|
+
return {
|
|
32
|
+
coin: p.coin,
|
|
33
|
+
size: Math.abs(szi),
|
|
34
|
+
side: szi > 0 ? "long" : szi < 0 ? "short" : "flat",
|
|
35
|
+
entryPx: Number(p.entryPx ?? 0),
|
|
36
|
+
positionValue: Number(p.positionValue ?? 0),
|
|
37
|
+
unrealizedPnl: Number(p.unrealizedPnl ?? 0),
|
|
38
|
+
returnOnEquity: Number(p.returnOnEquity ?? 0),
|
|
39
|
+
leverage: Number(p.leverage?.value ?? 0),
|
|
40
|
+
liquidationPx: p.liquidationPx != null ? Number(p.liquidationPx) : null,
|
|
41
|
+
marginUsed: Number(p.marginUsed ?? 0),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Parse an L2 book into a best bid/offer + mid + spread. */
|
|
45
|
+
export function parseBbo(book) {
|
|
46
|
+
const bidPx = book.levels?.[0]?.[0]?.px;
|
|
47
|
+
const askPx = book.levels?.[1]?.[0]?.px;
|
|
48
|
+
const bid = bidPx != null ? Number(bidPx) : null;
|
|
49
|
+
const ask = askPx != null ? Number(askPx) : null;
|
|
50
|
+
return {
|
|
51
|
+
bid,
|
|
52
|
+
ask,
|
|
53
|
+
mid: bid != null && ask != null ? (bid + ask) / 2 : null,
|
|
54
|
+
spread: bid != null && ask != null ? ask - bid : null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
28
57
|
function truthy(v) {
|
|
29
58
|
return v === true || v === 1 || v === "1" || v === "true";
|
|
30
59
|
}
|
|
@@ -206,6 +235,47 @@ export class Hyperliquid {
|
|
|
206
235
|
const startTime = opts.startTime ?? endTime - (opts.lookbackMs ?? 24 * 60 * 60 * 1000);
|
|
207
236
|
return this._info.candleSnapshot({ coin, interval, startTime, endTime });
|
|
208
237
|
}
|
|
238
|
+
/** Recent public trades for a coin. */
|
|
239
|
+
async recentTrades(coin) {
|
|
240
|
+
return this._info.recentTrades({ coin });
|
|
241
|
+
}
|
|
242
|
+
/** Best bid/offer + mid + spread for a coin (parsed from the L2 book). */
|
|
243
|
+
async bbo(coin) {
|
|
244
|
+
return parseBbo((await this.book(coin)));
|
|
245
|
+
}
|
|
246
|
+
/** All open positions as clean PositionInfo[] (default: the signer's account). */
|
|
247
|
+
async positionsList(user) {
|
|
248
|
+
const s = (await this.positions(user));
|
|
249
|
+
return (s.assetPositions ?? []).map((ap) => parseHlPosition(ap.position));
|
|
250
|
+
}
|
|
251
|
+
/** A single open position for a coin, or null if flat. */
|
|
252
|
+
async position(coin, user) {
|
|
253
|
+
return (await this.positionsList(user)).find((p) => p.coin === coin) ?? null;
|
|
254
|
+
}
|
|
255
|
+
/** Current hourly funding rate for a coin (fraction, e.g. 0.0000125). */
|
|
256
|
+
async fundingRate(coin) {
|
|
257
|
+
return (await this.assetCtx(coin)).funding;
|
|
258
|
+
}
|
|
259
|
+
/** Historical funding rates for a coin (default: the last 7 days). */
|
|
260
|
+
async fundingHistory(coin, opts = {}) {
|
|
261
|
+
const endTime = opts.endTime ?? Date.now();
|
|
262
|
+
const startTime = opts.startTime ?? endTime - (opts.lookbackMs ?? 7 * 24 * 60 * 60 * 1000);
|
|
263
|
+
return this._info.fundingHistory({ coin, startTime, endTime });
|
|
264
|
+
}
|
|
265
|
+
/** Funding payments paid/received by an account (default signer; last 7 days). */
|
|
266
|
+
async userFunding(opts = {}, user) {
|
|
267
|
+
const endTime = opts.endTime ?? Date.now();
|
|
268
|
+
const startTime = opts.startTime ?? endTime - (opts.lookbackMs ?? 7 * 24 * 60 * 60 * 1000);
|
|
269
|
+
return this._info.userFunding({ user: this.requireUser(user), startTime, endTime });
|
|
270
|
+
}
|
|
271
|
+
/** Predicted next funding rates across venues (no key). */
|
|
272
|
+
async predictedFundings() {
|
|
273
|
+
return this._info.predictedFundings();
|
|
274
|
+
}
|
|
275
|
+
/** Portfolio history (PnL/account-value time series) for an account (default: the signer). */
|
|
276
|
+
async portfolio(user) {
|
|
277
|
+
return this._info.portfolio({ user: this.requireUser(user) });
|
|
278
|
+
}
|
|
209
279
|
// ---- trading (key required) ----
|
|
210
280
|
/** Raw order passthrough (the @nktkas `order` shape) for full control. */
|
|
211
281
|
async order(params) {
|
|
@@ -281,10 +351,134 @@ export class Hyperliquid {
|
|
|
281
351
|
async cancel(coin, oid) {
|
|
282
352
|
return this.exchange.cancel({ cancels: [{ a: await this.assetId(coin), o: oid }] });
|
|
283
353
|
}
|
|
354
|
+
/** Cancel a resting order by its client order id (cloid). */
|
|
355
|
+
async cancelByCloid(coin, cloid) {
|
|
356
|
+
return this.exchange.cancelByCloid({ cancels: [{ asset: await this.assetId(coin), cloid }] });
|
|
357
|
+
}
|
|
358
|
+
/** Cancel ALL resting orders (optionally only for one coin). Returns the count cancelled. */
|
|
359
|
+
async cancelAll(coin) {
|
|
360
|
+
const orders = (await this.openOrders());
|
|
361
|
+
const targets = coin ? orders.filter((o) => o.coin === coin) : orders;
|
|
362
|
+
if (!targets.length)
|
|
363
|
+
return { cancelled: 0 };
|
|
364
|
+
const cancels = await Promise.all(targets.map(async (o) => ({ a: await this.assetId(o.coin), o: o.oid })));
|
|
365
|
+
await this.exchange.cancel({ cancels });
|
|
366
|
+
return { cancelled: targets.length };
|
|
367
|
+
}
|
|
368
|
+
/** Modify a resting limit order in place (new size/price/side). */
|
|
369
|
+
async modifyOrder(oid, coin, isBuy, size, price, opts = {}) {
|
|
370
|
+
const { id, szDecimals } = await this.lookup(coin);
|
|
371
|
+
return this.exchange.modify({
|
|
372
|
+
oid,
|
|
373
|
+
order: {
|
|
374
|
+
a: id,
|
|
375
|
+
b: isBuy,
|
|
376
|
+
p: formatPrice(price),
|
|
377
|
+
s: formatSize(size, szDecimals),
|
|
378
|
+
r: opts.reduceOnly ?? false,
|
|
379
|
+
t: { limit: { tif: opts.tif ?? "Gtc" } },
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Place a trigger order (stop / take-profit). `triggerPx` is the activation price;
|
|
385
|
+
* `tpsl` is "sl" (stop) or "tp". Market trigger by default (fills at market once
|
|
386
|
+
* armed); reduce-only by default (closing). `isBuy` is the side that executes —
|
|
387
|
+
* to protect a LONG use isBuy=false (sell), for a SHORT use isBuy=true (buy).
|
|
388
|
+
*/
|
|
389
|
+
async trigger(coin, isBuy, size, triggerPx, opts) {
|
|
390
|
+
const { id, szDecimals } = await this.lookup(coin);
|
|
391
|
+
return this.exchange.order({
|
|
392
|
+
orders: [
|
|
393
|
+
{
|
|
394
|
+
a: id,
|
|
395
|
+
b: isBuy,
|
|
396
|
+
p: formatPrice(opts.price ?? triggerPx),
|
|
397
|
+
s: formatSize(size, szDecimals),
|
|
398
|
+
r: opts.reduceOnly ?? true,
|
|
399
|
+
t: { trigger: { isMarket: opts.isMarket ?? true, triggerPx: formatPrice(triggerPx), tpsl: opts.tpsl } },
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
grouping: "na",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
/** Stop-loss trigger (reduce-only market by default). See `trigger` for `isBuy`. */
|
|
406
|
+
stopLoss(coin, isBuy, size, triggerPx, opts = {}) {
|
|
407
|
+
return this.trigger(coin, isBuy, size, triggerPx, { ...opts, tpsl: "sl" });
|
|
408
|
+
}
|
|
409
|
+
/** Take-profit trigger (reduce-only market by default). See `trigger` for `isBuy`. */
|
|
410
|
+
takeProfit(coin, isBuy, size, triggerPx, opts = {}) {
|
|
411
|
+
return this.trigger(coin, isBuy, size, triggerPx, { ...opts, tpsl: "tp" });
|
|
412
|
+
}
|
|
413
|
+
/** Dead-man's switch: auto-cancel all orders at `timeMs` (epoch) unless re-armed. Omit to clear. */
|
|
414
|
+
async scheduleCancel(timeMs) {
|
|
415
|
+
return this.exchange.scheduleCancel(timeMs ? { time: timeMs } : {});
|
|
416
|
+
}
|
|
417
|
+
/** Move USDC from spot into the perp wallet (margin top-up; not a withdrawal). */
|
|
418
|
+
async transferToPerp(usd) {
|
|
419
|
+
return this.exchange.usdClassTransfer({ amount: String(usd), toPerp: true });
|
|
420
|
+
}
|
|
421
|
+
/** Move USDC from the perp wallet back to spot. */
|
|
422
|
+
async transferToSpot(usd) {
|
|
423
|
+
return this.exchange.usdClassTransfer({ amount: String(usd), toPerp: false });
|
|
424
|
+
}
|
|
284
425
|
/** Set leverage for a coin (cross by default). */
|
|
285
426
|
async updateLeverage(coin, leverage, opts = {}) {
|
|
286
427
|
return this.exchange.updateLeverage({ asset: await this.assetId(coin), isCross: opts.cross ?? true, leverage });
|
|
287
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Place a TWAP order — slice `size` evenly over `minutes` (5–1440) to reduce impact.
|
|
431
|
+
* `randomize` jitters slice timing. Returns the @nktkas response (incl. the TWAP id).
|
|
432
|
+
*/
|
|
433
|
+
async twap(coin, isBuy, size, minutes, opts = {}) {
|
|
434
|
+
const { id, szDecimals } = await this.lookup(coin);
|
|
435
|
+
return this.exchange.twapOrder({
|
|
436
|
+
twap: { a: id, b: isBuy, s: formatSize(size, szDecimals), r: opts.reduceOnly ?? false, m: minutes, t: opts.randomize ?? false },
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
/** Cancel a running TWAP by its id (from the `twap()` response). */
|
|
440
|
+
async cancelTwap(coin, twapId) {
|
|
441
|
+
return this.exchange.twapCancel({ a: await this.assetId(coin), t: twapId });
|
|
442
|
+
}
|
|
443
|
+
// ---- sub-accounts (isolate strategies under one master) ----
|
|
444
|
+
/** Create a named sub-account (1–16 chars). Returns its address. */
|
|
445
|
+
async createSubAccount(name) {
|
|
446
|
+
return this.exchange.createSubAccount({ name });
|
|
447
|
+
}
|
|
448
|
+
/** List sub-accounts for an address (default: the signer). */
|
|
449
|
+
async subAccounts(user) {
|
|
450
|
+
return this._info.subAccounts({ user: this.requireUser(user) });
|
|
451
|
+
}
|
|
452
|
+
/** Move USDC from the master into a sub-account (`usd` in dollars). */
|
|
453
|
+
async transferToSubAccount(subAccount, usd) {
|
|
454
|
+
return this.exchange.subAccountTransfer({ subAccountUser: subAccount, isDeposit: true, usd: Math.round(usd * 1e6) });
|
|
455
|
+
}
|
|
456
|
+
/** Move USDC from a sub-account back to the master (`usd` in dollars). */
|
|
457
|
+
async transferFromSubAccount(subAccount, usd) {
|
|
458
|
+
return this.exchange.subAccountTransfer({ subAccountUser: subAccount, isDeposit: false, usd: Math.round(usd * 1e6) });
|
|
459
|
+
}
|
|
460
|
+
// ---- vaults (deposit into / withdraw from a Hyperliquid vault) ----
|
|
461
|
+
/** Deposit USDC into a vault (`usd` in dollars). */
|
|
462
|
+
async vaultDeposit(vault, usd) {
|
|
463
|
+
return this.exchange.vaultTransfer({ vaultAddress: vault, isDeposit: true, usd: Math.round(usd * 1e6) });
|
|
464
|
+
}
|
|
465
|
+
/** Withdraw USDC from a vault (`usd` in dollars). */
|
|
466
|
+
async vaultWithdraw(vault, usd) {
|
|
467
|
+
return this.exchange.vaultTransfer({ vaultAddress: vault, isDeposit: false, usd: Math.round(usd * 1e6) });
|
|
468
|
+
}
|
|
469
|
+
/** Vault details + your equity in it (public read; pass a user or use the signer). */
|
|
470
|
+
async vaultDetails(vault, user) {
|
|
471
|
+
return this._info.vaultDetails({ vaultAddress: vault, user: (user ?? this.address ?? undefined) });
|
|
472
|
+
}
|
|
473
|
+
// ---- account info ----
|
|
474
|
+
/** Your fee schedule + 14-day volume (default: the signer). */
|
|
475
|
+
async userFees(user) {
|
|
476
|
+
return this._info.userFees({ user: this.requireUser(user) });
|
|
477
|
+
}
|
|
478
|
+
/** Status of a single order by its oid (resting/filled/canceled). */
|
|
479
|
+
async orderStatus(oid, user) {
|
|
480
|
+
return this._info.orderStatus({ user: this.requireUser(user), oid });
|
|
481
|
+
}
|
|
288
482
|
// The @nktkas SymbolConverter resolves asset ids + szDecimals across the main
|
|
289
483
|
// perp dex, spot, AND HIP-3 builder dexes (so "xyz:TSLA" → its global asset id).
|
|
290
484
|
symbols() {
|
package/dist/hyperliquid.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Hyperliquid agent-wallet delegation: per-user agent keys must be deterministic
|
|
2
2
|
// (recomputable, never stored) and the bound client must sign as the agent.
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { Hyperliquid } from "./hyperliquid.js";
|
|
4
|
+
import { Hyperliquid, parseHlPosition, parseBbo } from "./hyperliquid.js";
|
|
5
5
|
import { localAccount } from "./eip712.js";
|
|
6
6
|
const SECRET = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
|
|
7
7
|
const MASTER = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826";
|
|
@@ -29,3 +29,19 @@ describe("Hyperliquid agent-wallet delegation", () => {
|
|
|
29
29
|
expect(() => bare.agentKey("user-1")).toThrow(/agent secret/i);
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
|
+
describe("Hyperliquid parsers", () => {
|
|
33
|
+
it("parseHlPosition normalizes a clearinghouse position (side/size/fields)", () => {
|
|
34
|
+
const long = parseHlPosition({ coin: "BTC", szi: "0.5", entryPx: "60000", positionValue: "31000", unrealizedPnl: "1000", returnOnEquity: "0.12", leverage: { value: 10 }, liquidationPx: "40000", marginUsed: "3100" });
|
|
35
|
+
expect(long).toMatchObject({ coin: "BTC", size: 0.5, side: "long", entryPx: 60000, unrealizedPnl: 1000, leverage: 10, liquidationPx: 40000 });
|
|
36
|
+
const short = parseHlPosition({ coin: "ETH", szi: "-2" });
|
|
37
|
+
expect(short.side).toBe("short");
|
|
38
|
+
expect(short.size).toBe(2);
|
|
39
|
+
expect(short.liquidationPx).toBeNull();
|
|
40
|
+
expect(parseHlPosition({ coin: "X", szi: "0" }).side).toBe("flat");
|
|
41
|
+
});
|
|
42
|
+
it("parseBbo derives bid/ask/mid/spread from the L2 book", () => {
|
|
43
|
+
const bbo = parseBbo({ levels: [[{ px: "99.5" }], [{ px: "100.5" }]] });
|
|
44
|
+
expect(bbo).toEqual({ bid: 99.5, ask: 100.5, mid: 100, spread: 1 });
|
|
45
|
+
expect(parseBbo({ levels: [[], []] })).toEqual({ bid: null, ask: null, mid: null, spread: null });
|
|
46
|
+
});
|
|
47
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { watchLogs } from "./watch.js";
|
|
|
6
6
|
export type { WatchLogsOptions, WatchLogsResult } from "./watch.js";
|
|
7
7
|
export { defineWatcher } from "./watcher.js";
|
|
8
8
|
export type { WatcherEnv, WatcherMatch, WatcherScope, WatcherContext, WatcherDefinition, WatcherModule, SubscribeOptions, Confidence, MatchStatus, } from "./watcher.js";
|
|
9
|
+
export { Signals, signals, DEFAULT_SIGNALS_URL } from "./signals.js";
|
|
10
|
+
export type { SignalsOptions, Market, SignalMatch, Swap, Launch, EngineSnapshot, MarketFilter, } from "./signals.js";
|
|
9
11
|
export { Relayer } from "./relayer.js";
|
|
10
12
|
export { Queue, queue, queueEnvKey } from "./queue.js";
|
|
11
13
|
export type { RelayerOptions } from "./relayer.js";
|
|
@@ -21,8 +23,25 @@ export { TelegramLinks, TELEGRAM_LINKS_TABLE } from "./telegramLinks.js";
|
|
|
21
23
|
export type { TelegramLink } from "./telegramLinks.js";
|
|
22
24
|
export { tokenBalanceOf, meetsGate } from "./gate.js";
|
|
23
25
|
export type { TokenGate } from "./gate.js";
|
|
26
|
+
export { tokenMetadata, ownerOf, tokenURI } from "./reads.js";
|
|
27
|
+
export type { TokenMetadata } from "./reads.js";
|
|
28
|
+
export { resolveUri, fetchTokenMetadata } from "./nft.js";
|
|
29
|
+
export type { NftMetadata, NftAttribute, FetchMetadataOptions } from "./nft.js";
|
|
30
|
+
export { createSiweMessage, verifySiweMessage } from "./siwe.js";
|
|
31
|
+
export type { SiweParams, SiweVerifyResult } from "./siwe.js";
|
|
32
|
+
export { createSession, verifySession } from "./sessions.js";
|
|
33
|
+
export type { SessionOptions } from "./sessions.js";
|
|
34
|
+
export { json, error, withCors, CORS_HEADERS, Router, sse } from "./http.js";
|
|
35
|
+
export type { RouteContext, RouteHandler, SseConnection } from "./http.js";
|
|
36
|
+
export { rateLimit } from "./ratelimit.js";
|
|
37
|
+
export type { KvLike, RateLimitOptions, RateLimitResult } from "./ratelimit.js";
|
|
38
|
+
export { encrypt, decrypt } from "./aes.js";
|
|
39
|
+
export { cached } from "./cache.js";
|
|
40
|
+
export type { CacheOptions } from "./cache.js";
|
|
41
|
+
export { verifyWebhook, signWebhook } from "./webhook.js";
|
|
42
|
+
export type { VerifyWebhookParams, SignatureEncoding } from "./webhook.js";
|
|
24
43
|
export { Hyperliquid } from "./hyperliquid.js";
|
|
25
|
-
export type { HyperliquidOptions, PlaceOrderOptions, MarketOrderOptions, Tif, CandleInterval, CandlesOptions, AssetContext, AccountBalance, } from "./hyperliquid.js";
|
|
44
|
+
export type { HyperliquidOptions, PlaceOrderOptions, MarketOrderOptions, Tif, CandleInterval, CandlesOptions, AssetContext, AccountBalance, PositionInfo, Bbo, } from "./hyperliquid.js";
|
|
26
45
|
export { Polymarket } from "./polymarket.js";
|
|
27
46
|
export type { PolymarketOptions, PolymarketCreds, PlaceOrderParams, Side, OrderType, SignatureType, PriceInterval, PriceHistoryOptions, MarketOutcome, ResolvedMarket, } from "./polymarket.js";
|
|
28
47
|
export { hashTypedData, signTypedData, localAccount } from "./eip712.js";
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,10 @@ export { watchLogs } from "./watch.js";
|
|
|
10
10
|
// defineWatcher — onMatch primitive for Signal Radar watchers (HTTP-triggered,
|
|
11
11
|
// one subscription-addressed match per request) + ctx.subscribe/cancel.
|
|
12
12
|
export { defineWatcher } from "./watcher.js";
|
|
13
|
+
// Signals — READ client for the shared on-chain signals engine (Signal Radar):
|
|
14
|
+
// live markets, recent swaps, fired matches. Zero-config (defaults to the public
|
|
15
|
+
// engine). The push counterpart is defineWatcher above.
|
|
16
|
+
export { Signals, signals, DEFAULT_SIGNALS_URL } from "./signals.js";
|
|
13
17
|
// Relayer — managed signing for bots (custodied RELAYER_PRIVATE_KEY + optional
|
|
14
18
|
// Convex-serialized nonces). A Chain that defaults to the relayer key.
|
|
15
19
|
export { Relayer } from "./relayer.js";
|
|
@@ -26,6 +30,15 @@ export { Telegram } from "./telegram.js";
|
|
|
26
30
|
export { verifyTelegramInitData, verifyTelegramLoginWidget, parseStartPayload } from "./telegramAuth.js";
|
|
27
31
|
export { TelegramLinks, TELEGRAM_LINKS_TABLE } from "./telegramLinks.js";
|
|
28
32
|
export { tokenBalanceOf, meetsGate } from "./gate.js";
|
|
33
|
+
export { tokenMetadata, ownerOf, tokenURI } from "./reads.js";
|
|
34
|
+
export { resolveUri, fetchTokenMetadata } from "./nft.js";
|
|
35
|
+
export { createSiweMessage, verifySiweMessage } from "./siwe.js";
|
|
36
|
+
export { createSession, verifySession } from "./sessions.js";
|
|
37
|
+
export { json, error, withCors, CORS_HEADERS, Router, sse } from "./http.js";
|
|
38
|
+
export { rateLimit } from "./ratelimit.js";
|
|
39
|
+
export { encrypt, decrypt } from "./aes.js";
|
|
40
|
+
export { cached } from "./cache.js";
|
|
41
|
+
export { verifyWebhook, signWebhook } from "./webhook.js";
|
|
29
42
|
// Hyperliquid — perps/spot data + trading (wraps @nktkas/hyperliquid; signs with
|
|
30
43
|
// the runtime's EIP-712 signer, no viem/ethers in the bundle).
|
|
31
44
|
export { Hyperliquid } from "./hyperliquid.js";
|
package/dist/nft.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Chain } from "./chain.js";
|
|
2
|
+
export interface NftAttribute {
|
|
3
|
+
trait_type?: string;
|
|
4
|
+
value?: unknown;
|
|
5
|
+
[k: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface NftMetadata {
|
|
8
|
+
name?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Image URL resolved through the gateway (ipfs:// → https). */
|
|
11
|
+
image?: string;
|
|
12
|
+
/** The original, unresolved image value from the metadata document. */
|
|
13
|
+
imageRaw?: string;
|
|
14
|
+
/** animation_url resolved through the gateway (video/audio/3D media). */
|
|
15
|
+
animationUrl?: string;
|
|
16
|
+
attributes?: NftAttribute[];
|
|
17
|
+
/** The full parsed metadata document. */
|
|
18
|
+
raw: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface FetchMetadataOptions {
|
|
21
|
+
/** IPFS gateway base, trailing slash included. Default "https://ipfs.io/ipfs/". */
|
|
22
|
+
gateway?: string;
|
|
23
|
+
/** ERC-1155 reads uri(id) with {id} substitution; default ERC-721 tokenURI(id). */
|
|
24
|
+
standard?: "erc721" | "erc1155";
|
|
25
|
+
}
|
|
26
|
+
/** Resolve an ipfs:// / ar:// URI to an HTTP(S) URL. http(s)/data URIs pass through. */
|
|
27
|
+
export declare function resolveUri(uri: string, gateway?: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Read a token's metadata URI on-chain, resolve + fetch + parse it, and resolve
|
|
30
|
+
* the embedded image/animation URLs. Returns null if the URI read reverts (e.g.
|
|
31
|
+
* unminted). Throws if the metadata document itself can't be fetched/parsed.
|
|
32
|
+
*/
|
|
33
|
+
export declare function fetchTokenMetadata(chain: Chain, token: string, tokenId: bigint, opts?: FetchMetadataOptions): Promise<NftMetadata | null>;
|
package/dist/nft.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// NFT metadata — read a token's metadata URI on-chain, resolve it (ipfs:// →
|
|
2
|
+
// gateway), fetch + parse the JSON, and resolve the embedded media URLs. Works for
|
|
3
|
+
// ERC-721 (tokenURI) and ERC-1155 (uri(id) with {id} substitution). Pairs with the
|
|
4
|
+
// kit's useNFT / <NFTCard>.
|
|
5
|
+
const DEFAULT_GATEWAY = "https://ipfs.io/ipfs/";
|
|
6
|
+
/** Resolve an ipfs:// / ar:// URI to an HTTP(S) URL. http(s)/data URIs pass through. */
|
|
7
|
+
export function resolveUri(uri, gateway = DEFAULT_GATEWAY) {
|
|
8
|
+
if (!uri)
|
|
9
|
+
return uri;
|
|
10
|
+
if (uri.startsWith("ipfs://")) {
|
|
11
|
+
let rest = uri.slice("ipfs://".length);
|
|
12
|
+
if (rest.startsWith("ipfs/"))
|
|
13
|
+
rest = rest.slice("ipfs/".length); // odd ipfs://ipfs/CID form
|
|
14
|
+
return gateway + rest;
|
|
15
|
+
}
|
|
16
|
+
if (uri.startsWith("ar://"))
|
|
17
|
+
return "https://arweave.net/" + uri.slice("ar://".length);
|
|
18
|
+
return uri;
|
|
19
|
+
}
|
|
20
|
+
/** ERC-1155 {id} substitution token: lowercase hex, zero-padded to 64 chars. */
|
|
21
|
+
function erc1155IdHex(tokenId) {
|
|
22
|
+
return tokenId.toString(16).padStart(64, "0");
|
|
23
|
+
}
|
|
24
|
+
async function loadJson(uri) {
|
|
25
|
+
if (uri.startsWith("data:")) {
|
|
26
|
+
const comma = uri.indexOf(",");
|
|
27
|
+
const header = uri.slice(5, comma);
|
|
28
|
+
const body = uri.slice(comma + 1);
|
|
29
|
+
const text = header.includes("base64") ? atob(body) : decodeURIComponent(body);
|
|
30
|
+
return JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
const res = await fetch(uri);
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
throw new Error(`metadata fetch ${res.status} for ${uri}`);
|
|
35
|
+
return (await res.json());
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read a token's metadata URI on-chain, resolve + fetch + parse it, and resolve
|
|
39
|
+
* the embedded image/animation URLs. Returns null if the URI read reverts (e.g.
|
|
40
|
+
* unminted). Throws if the metadata document itself can't be fetched/parsed.
|
|
41
|
+
*/
|
|
42
|
+
export async function fetchTokenMetadata(chain, token, tokenId, opts = {}) {
|
|
43
|
+
const gateway = opts.gateway ?? DEFAULT_GATEWAY;
|
|
44
|
+
const t = token;
|
|
45
|
+
let uri;
|
|
46
|
+
try {
|
|
47
|
+
if (opts.standard === "erc1155") {
|
|
48
|
+
const raw = (await chain.call(t, "uri(uint256)(string)", [tokenId]));
|
|
49
|
+
uri = raw?.replace(/\{id\}/g, erc1155IdHex(tokenId));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
uri = (await chain.call(t, "tokenURI(uint256)(string)", [tokenId]));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (!uri)
|
|
59
|
+
return null;
|
|
60
|
+
const doc = await loadJson(resolveUri(uri, gateway));
|
|
61
|
+
const imageRaw = (doc.image ?? doc.image_url ?? doc.imageUrl);
|
|
62
|
+
const animationRaw = (doc.animation_url ?? doc.animationUrl);
|
|
63
|
+
return {
|
|
64
|
+
name: doc.name,
|
|
65
|
+
description: doc.description,
|
|
66
|
+
image: imageRaw ? resolveUri(imageRaw, gateway) : undefined,
|
|
67
|
+
imageRaw,
|
|
68
|
+
animationUrl: animationRaw ? resolveUri(animationRaw, gateway) : undefined,
|
|
69
|
+
attributes: doc.attributes ?? undefined,
|
|
70
|
+
raw: doc,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/nft.test.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveUri, fetchTokenMetadata } from "./nft.js";
|
|
3
|
+
describe("resolveUri", () => {
|
|
4
|
+
it("maps ipfs:// (and ipfs://ipfs/) to the gateway, ar:// to arweave, passes http/data through", () => {
|
|
5
|
+
expect(resolveUri("ipfs://bafyCID/1.json")).toBe("https://ipfs.io/ipfs/bafyCID/1.json");
|
|
6
|
+
expect(resolveUri("ipfs://ipfs/bafyCID")).toBe("https://ipfs.io/ipfs/bafyCID");
|
|
7
|
+
expect(resolveUri("ipfs://CID", "https://x.mypinata.cloud/ipfs/")).toBe("https://x.mypinata.cloud/ipfs/CID");
|
|
8
|
+
expect(resolveUri("ar://TXID")).toBe("https://arweave.net/TXID");
|
|
9
|
+
expect(resolveUri("https://example.com/1.json")).toBe("https://example.com/1.json");
|
|
10
|
+
expect(resolveUri("data:application/json,{}")).toBe("data:application/json,{}");
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
// A fake Chain that returns a canned tokenURI/uri string — no network, no RPC.
|
|
14
|
+
function fakeChain(uri) {
|
|
15
|
+
return { call: async () => uri };
|
|
16
|
+
}
|
|
17
|
+
describe("fetchTokenMetadata", () => {
|
|
18
|
+
it("reads tokenURI, parses a base64 data: doc, and resolves the ipfs image", async () => {
|
|
19
|
+
const doc = { name: "Glyph #7", description: "x", image: "ipfs://imgCID/7.png", attributes: [{ trait_type: "Eyes", value: "Laser" }] };
|
|
20
|
+
const dataUri = "data:application/json;base64," + btoa(JSON.stringify(doc));
|
|
21
|
+
const m = await fetchTokenMetadata(fakeChain(dataUri), "0xabc", 7n);
|
|
22
|
+
expect(m?.name).toBe("Glyph #7");
|
|
23
|
+
expect(m?.image).toBe("https://ipfs.io/ipfs/imgCID/7.png");
|
|
24
|
+
expect(m?.imageRaw).toBe("ipfs://imgCID/7.png");
|
|
25
|
+
expect(m?.attributes?.[0]).toEqual({ trait_type: "Eyes", value: "Laser" });
|
|
26
|
+
});
|
|
27
|
+
it("parses a plain (url-encoded) data: doc and image_url fallback", async () => {
|
|
28
|
+
const doc = { name: "B", image_url: "https://cdn/x.png" };
|
|
29
|
+
const dataUri = "data:application/json," + encodeURIComponent(JSON.stringify(doc));
|
|
30
|
+
const m = await fetchTokenMetadata(fakeChain(dataUri), "0xabc", 1n);
|
|
31
|
+
expect(m?.image).toBe("https://cdn/x.png");
|
|
32
|
+
});
|
|
33
|
+
it("substitutes {id} (64-hex padded) for ERC-1155 uris before fetching", async () => {
|
|
34
|
+
// uri(id) returns a template with {id}; we replace it across the whole URI. Here the
|
|
35
|
+
// literal {id} sits in a plain data: body so the substituted value is observable.
|
|
36
|
+
const uri = 'data:application/json,{"name":"id-{id}"}';
|
|
37
|
+
const m = await fetchTokenMetadata(fakeChain(uri), "0xabc", 1n, { standard: "erc1155" });
|
|
38
|
+
expect(m?.name).toBe("id-0000000000000000000000000000000000000000000000000000000000000001");
|
|
39
|
+
});
|
|
40
|
+
it("returns null when the URI read reverts", async () => {
|
|
41
|
+
const reverting = { call: async () => { throw new Error("revert"); } };
|
|
42
|
+
expect(await fetchTokenMetadata(reverting, "0xabc", 1n)).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface KvLike {
|
|
2
|
+
get(key: string): Promise<string | null>;
|
|
3
|
+
put(key: string, value: string, options?: {
|
|
4
|
+
expirationTtl?: number;
|
|
5
|
+
}): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface RateLimitOptions {
|
|
8
|
+
/** Max requests allowed in the window. */
|
|
9
|
+
limit: number;
|
|
10
|
+
/** Window length in seconds. */
|
|
11
|
+
windowSeconds: number;
|
|
12
|
+
/** Key prefix (default "rl"). */
|
|
13
|
+
prefix?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RateLimitResult {
|
|
16
|
+
allowed: boolean;
|
|
17
|
+
remaining: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function rateLimit(kv: KvLike, key: string, opts: RateLimitOptions): Promise<RateLimitResult>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// A KV-backed fixed-window rate limiter for bots / api Workers (e.g. per wallet or
|
|
2
|
+
// per Telegram user). Pass the project's env.KV.
|
|
3
|
+
// Increment the counter for `key`; allow until `limit` is reached within the window.
|
|
4
|
+
export async function rateLimit(kv, key, opts) {
|
|
5
|
+
const k = `${opts.prefix ?? "rl"}:${key}`;
|
|
6
|
+
const current = Number((await kv.get(k)) ?? "0");
|
|
7
|
+
if (current >= opts.limit)
|
|
8
|
+
return { allowed: false, remaining: 0 };
|
|
9
|
+
await kv.put(k, String(current + 1), { expirationTtl: opts.windowSeconds });
|
|
10
|
+
return { allowed: true, remaining: Math.max(0, opts.limit - current - 1) };
|
|
11
|
+
}
|
package/dist/reads.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Chain } from "./chain.js";
|
|
2
|
+
export interface TokenMetadata {
|
|
3
|
+
name?: string;
|
|
4
|
+
symbol?: string;
|
|
5
|
+
decimals?: number;
|
|
6
|
+
}
|
|
7
|
+
/** ERC-20 name / symbol / decimals (each best-effort — missing fields stay undefined). */
|
|
8
|
+
export declare function tokenMetadata(chain: Chain, token: string): Promise<TokenMetadata>;
|
|
9
|
+
/** Owner of an ERC-721 token id (or null if the read reverts, e.g. unminted). */
|
|
10
|
+
export declare function ownerOf(chain: Chain, token: string, tokenId: bigint): Promise<string | null>;
|
|
11
|
+
/** ERC-721 tokenURI for a token id (or null on revert). */
|
|
12
|
+
export declare function tokenURI(chain: Chain, token: string, tokenId: bigint): Promise<string | null>;
|
package/dist/reads.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Convenience contract reads on top of Chain.call — ERC-20 metadata + ERC-721
|
|
2
|
+
// ownership. (For balances use tokenBalanceOf from ./gate; for native balance,
|
|
3
|
+
// block number and receipts use chain.getBalance / chain.blockNumber /
|
|
4
|
+
// chain.waitForReceipt directly.)
|
|
5
|
+
/** ERC-20 name / symbol / decimals (each best-effort — missing fields stay undefined). */
|
|
6
|
+
export async function tokenMetadata(chain, token) {
|
|
7
|
+
const t = token;
|
|
8
|
+
const [name, symbol, decimals] = await Promise.all([
|
|
9
|
+
chain.call(t, "name()(string)").catch(() => undefined),
|
|
10
|
+
chain.call(t, "symbol()(string)").catch(() => undefined),
|
|
11
|
+
chain.call(t, "decimals()(uint8)").catch(() => undefined),
|
|
12
|
+
]);
|
|
13
|
+
return {
|
|
14
|
+
name: name,
|
|
15
|
+
symbol: symbol,
|
|
16
|
+
decimals: decimals !== undefined ? Number(decimals) : undefined,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Owner of an ERC-721 token id (or null if the read reverts, e.g. unminted). */
|
|
20
|
+
export async function ownerOf(chain, token, tokenId) {
|
|
21
|
+
try {
|
|
22
|
+
return (await chain.call(token, "ownerOf(uint256)(address)", [tokenId]));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** ERC-721 tokenURI for a token id (or null on revert). */
|
|
29
|
+
export async function tokenURI(chain, token, tokenId) {
|
|
30
|
+
try {
|
|
31
|
+
return (await chain.call(token, "tokenURI(uint256)(string)", [tokenId]));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SessionOptions {
|
|
2
|
+
/** Token lifetime in seconds (sets `exp`). Omit for non-expiring. */
|
|
3
|
+
ttlSeconds?: number;
|
|
4
|
+
}
|
|
5
|
+
/** Sign a payload into a `<payload>.<hmac>` token. */
|
|
6
|
+
export declare function createSession<T extends Record<string, unknown>>(payload: T, secret: string, opts?: SessionOptions): Promise<string>;
|
|
7
|
+
/** Verify + decode a session token. Returns the payload, or null if bad/expired. */
|
|
8
|
+
export declare function verifySession<T = Record<string, unknown>>(token: string, secret: string): Promise<T | null>;
|
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Stateless signed sessions — an HMAC-SHA256 token (like a minimal JWT) you issue
|
|
2
|
+
// after wallet auth and verify on later requests. Worker-safe (Web Crypto, base64url).
|
|
3
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
4
|
+
function b64url(bytes) {
|
|
5
|
+
let bin = "";
|
|
6
|
+
for (let i = 0; i < bytes.length; i++)
|
|
7
|
+
bin += String.fromCharCode(bytes[i]);
|
|
8
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
9
|
+
}
|
|
10
|
+
function unb64url(s) {
|
|
11
|
+
const p = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
12
|
+
const padded = p + "=".repeat((4 - (p.length % 4)) % 4);
|
|
13
|
+
const bin = atob(padded);
|
|
14
|
+
const out = new Uint8Array(bin.length);
|
|
15
|
+
for (let i = 0; i < bin.length; i++)
|
|
16
|
+
out[i] = bin.charCodeAt(i);
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
async function hmac(secret, msg) {
|
|
20
|
+
const key = await crypto.subtle.importKey("raw", enc(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
21
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", key, enc(msg)));
|
|
22
|
+
}
|
|
23
|
+
function safeEqual(a, b) {
|
|
24
|
+
if (a.length !== b.length)
|
|
25
|
+
return false;
|
|
26
|
+
let d = 0;
|
|
27
|
+
for (let i = 0; i < a.length; i++)
|
|
28
|
+
d |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
29
|
+
return d === 0;
|
|
30
|
+
}
|
|
31
|
+
/** Sign a payload into a `<payload>.<hmac>` token. */
|
|
32
|
+
export async function createSession(payload, secret, opts = {}) {
|
|
33
|
+
const body = { ...payload };
|
|
34
|
+
if (opts.ttlSeconds)
|
|
35
|
+
body.exp = Math.floor(Date.now() / 1000) + opts.ttlSeconds;
|
|
36
|
+
const p = b64url(enc(JSON.stringify(body)));
|
|
37
|
+
const sig = b64url(await hmac(secret, p));
|
|
38
|
+
return `${p}.${sig}`;
|
|
39
|
+
}
|
|
40
|
+
/** Verify + decode a session token. Returns the payload, or null if bad/expired. */
|
|
41
|
+
export async function verifySession(token, secret) {
|
|
42
|
+
const dot = token.indexOf(".");
|
|
43
|
+
if (dot < 0)
|
|
44
|
+
return null;
|
|
45
|
+
const p = token.slice(0, dot);
|
|
46
|
+
const sig = token.slice(dot + 1);
|
|
47
|
+
const expected = b64url(await hmac(secret, p));
|
|
48
|
+
if (!safeEqual(expected, sig))
|
|
49
|
+
return null;
|
|
50
|
+
let body;
|
|
51
|
+
try {
|
|
52
|
+
body = JSON.parse(new TextDecoder().decode(unb64url(p)));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (typeof body.exp === "number" && body.exp < Math.floor(Date.now() / 1000))
|
|
58
|
+
return null;
|
|
59
|
+
return body;
|
|
60
|
+
}
|