@livo-build/runtime 0.2.5 → 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.
Files changed (47) hide show
  1. package/dist/aes.d.ts +4 -0
  2. package/dist/aes.js +38 -0
  3. package/dist/auth.test.d.ts +1 -0
  4. package/dist/auth.test.js +57 -0
  5. package/dist/cache.d.ts +12 -0
  6. package/dist/cache.js +27 -0
  7. package/dist/gate.d.ts +10 -0
  8. package/dist/gate.js +18 -0
  9. package/dist/http.d.ts +48 -0
  10. package/dist/http.js +133 -0
  11. package/dist/hyperliquid.d.ts +269 -0
  12. package/dist/hyperliquid.js +194 -0
  13. package/dist/hyperliquid.test.js +17 -1
  14. package/dist/index.d.ts +27 -2
  15. package/dist/index.js +16 -0
  16. package/dist/nft.d.ts +33 -0
  17. package/dist/nft.js +72 -0
  18. package/dist/nft.test.d.ts +1 -0
  19. package/dist/nft.test.js +44 -0
  20. package/dist/polymarket.d.ts +12 -0
  21. package/dist/polymarket.js +19 -6
  22. package/dist/polymarket.test.js +10 -0
  23. package/dist/ratelimit.d.ts +19 -0
  24. package/dist/ratelimit.js +11 -0
  25. package/dist/reads.d.ts +12 -0
  26. package/dist/reads.js +36 -0
  27. package/dist/sessions.d.ts +8 -0
  28. package/dist/sessions.js +60 -0
  29. package/dist/signals.d.ts +131 -0
  30. package/dist/signals.js +146 -0
  31. package/dist/siwe.d.ts +24 -0
  32. package/dist/siwe.js +33 -0
  33. package/dist/sse.test.d.ts +1 -0
  34. package/dist/sse.test.js +28 -0
  35. package/dist/telegram.d.ts +31 -0
  36. package/dist/telegram.js +73 -0
  37. package/dist/telegramAuth.d.ts +16 -0
  38. package/dist/telegramAuth.js +108 -0
  39. package/dist/telegramAuth.test.d.ts +1 -0
  40. package/dist/telegramAuth.test.js +68 -0
  41. package/dist/telegramLinks.d.ts +27 -0
  42. package/dist/telegramLinks.js +78 -0
  43. package/dist/webhook.d.ts +18 -0
  44. package/dist/webhook.js +49 -0
  45. package/dist/webhook.test.d.ts +1 -0
  46. package/dist/webhook.test.js +46 -0
  47. package/package.json +14 -4
@@ -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() {
@@ -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";
@@ -14,9 +16,32 @@ export type { WalletOptions, WalletAccount, WalletSendOptions } from "./wallet.j
14
16
  export { Indexer, indexer, indexerEnvKey, GraphQLError } from "./indexer.js";
15
17
  export type { IndexerOptions } from "./indexer.js";
16
18
  export { Telegram } from "./telegram.js";
17
- export type { TelegramOptions, BotMessage, SendMessageOptions } from "./telegram.js";
19
+ export type { TelegramOptions, BotMessage, SendMessageOptions, ChatJoinRequest } from "./telegram.js";
20
+ export { verifyTelegramInitData, verifyTelegramLoginWidget, parseStartPayload } from "./telegramAuth.js";
21
+ export type { TelegramUser, VerifyOptions } from "./telegramAuth.js";
22
+ export { TelegramLinks, TELEGRAM_LINKS_TABLE } from "./telegramLinks.js";
23
+ export type { TelegramLink } from "./telegramLinks.js";
24
+ export { tokenBalanceOf, meetsGate } from "./gate.js";
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";
18
43
  export { Hyperliquid } from "./hyperliquid.js";
19
- 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";
20
45
  export { Polymarket } from "./polymarket.js";
21
46
  export type { PolymarketOptions, PolymarketCreds, PlaceOrderParams, Side, OrderType, SignatureType, PriceInterval, PriceHistoryOptions, MarketOutcome, ResolvedMarket, } from "./polymarket.js";
22
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";
@@ -23,6 +27,18 @@ export { Wallet, UserWallet } from "./wallet.js";
23
27
  export { Indexer, indexer, indexerEnvKey, GraphQLError } from "./indexer.js";
24
28
  // Telegram — webhook verification + reply plumbing for bots.
25
29
  export { Telegram } from "./telegram.js";
30
+ export { verifyTelegramInitData, verifyTelegramLoginWidget, parseStartPayload } from "./telegramAuth.js";
31
+ export { TelegramLinks, TELEGRAM_LINKS_TABLE } from "./telegramLinks.js";
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";
26
42
  // Hyperliquid — perps/spot data + trading (wraps @nktkas/hyperliquid; signs with
27
43
  // the runtime's EIP-712 signer, no viem/ethers in the bundle).
28
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 {};
@@ -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
+ });
@@ -38,6 +38,15 @@ export interface PolymarketOptions {
38
38
  * reads always work).
39
39
  */
40
40
  allowLegacyV1?: boolean;
41
+ /**
42
+ * Polymarket Builder/Relayer API key that authorizes V2 deposit-wallet deployment
43
+ * + order placement. On Livo this is a PLATFORM credential (one Livo-held builder
44
+ * key serving every project), injected as env.POLYMARKET_BUILDER_API_KEY — projects
45
+ * never set it. Default: env.POLYMARKET_BUILDER_API_KEY. (V2 order placement via
46
+ * @polymarket/client is not wired yet even when present — see
47
+ * docs/POLYMARKET-V2-IMPLEMENTATION.md.)
48
+ */
49
+ builderApiKey?: string;
41
50
  /** Signing key. Default: env.POLYMARKET_PRIVATE_KEY, then env.RELAYER_PRIVATE_KEY. */
42
51
  privateKey?: string;
43
52
  /** Env var name to read the key from. Default: "POLYMARKET_PRIVATE_KEY". */
@@ -85,6 +94,7 @@ export declare class Polymarket {
85
94
  private readonly host;
86
95
  private readonly privateKey?;
87
96
  private readonly legacyV1;
97
+ private readonly builderApiKey?;
88
98
  private readonly contracts;
89
99
  private creds?;
90
100
  constructor(env: MinimalEnv | undefined, options?: PolymarketOptions);
@@ -122,6 +132,8 @@ export declare class Polymarket {
122
132
  positions(user?: string): Promise<unknown>;
123
133
  /** Ensure API credentials exist (derive existing, else create). Cached on the instance. */
124
134
  ensureCreds(): Promise<PolymarketCreds>;
135
+ /** True once Livo's platform Polymarket builder key is configured (env.POLYMARKET_BUILDER_API_KEY). */
136
+ get builderConfigured(): boolean;
125
137
  private assertTradingEnabled;
126
138
  private deriveOrCreateApiKey;
127
139
  /** Build, sign, and submit an order. Returns the CLOB response JSON. */
@@ -171,12 +171,16 @@ export class Polymarket {
171
171
  host;
172
172
  privateKey;
173
173
  legacyV1;
174
+ builderApiKey;
174
175
  contracts;
175
176
  creds;
176
177
  constructor(env, options = {}) {
177
178
  this.chainId = options.chainId ?? Number(env?.POLYMARKET_CHAIN_ID ?? 137);
178
179
  this.host = options.host ?? env?.POLYMARKET_CLOB_HOST ?? CLOB_HOST;
179
180
  this.legacyV1 = options.allowLegacyV1 ?? (env?.POLYMARKET_ALLOW_LEGACY_V1 === "1" || env?.POLYMARKET_ALLOW_LEGACY_V1 === "true");
181
+ // Platform-injected Livo builder credential (one key for all projects). Read-only here;
182
+ // V2 order placement via @polymarket/client isn't wired yet, so this is detection only.
183
+ this.builderApiKey = options.builderApiKey ?? env?.POLYMARKET_BUILDER_API_KEY;
180
184
  this.contracts = this.legacyV1 ? CONTRACTS_V1 : CONTRACTS_V2;
181
185
  const keySecret = options.privateKeySecret ?? "POLYMARKET_PRIVATE_KEY";
182
186
  this.privateKey =
@@ -306,15 +310,24 @@ export class Polymarket {
306
310
  // All trading + L2-auth methods route through ensureCreds, so this one guard covers
307
311
  // them. Data reads never call it. V2 order signing isn't correctly implemented yet
308
312
  // (see the header note) — gate it rather than submit invalid orders.
313
+ /** True once Livo's platform Polymarket builder key is configured (env.POLYMARKET_BUILDER_API_KEY). */
314
+ get builderConfigured() {
315
+ return Boolean(this.builderApiKey);
316
+ }
309
317
  assertTradingEnabled() {
310
318
  if (this.legacyV1)
311
319
  return; // deprecated V1 path (testnet/legacy)
312
- throw new Error("Polymarket V2 trading isn't available through this helper. Verified live: V2 (Apr 2026) REQUIRES " +
313
- "the deposit-wallet flow (a plain EOA maker is rejected: 'use the deposit wallet flow'), and deploying " +
314
- "a deposit wallet needs a Polymarket BUILDER or RELAYER API key — a credential Polymarket issues to " +
315
- "builders, not something derivable here. The path is to wrap the official @polymarket/client SDK with " +
316
- "that key (see docs/POLYMARKET-V2-IMPLEMENTATION.md). Market-DATA reads work without any of this. " +
317
- "{ allowLegacyV1: true } enables the deprecated V1 path on testnet only.");
320
+ // V2 trading needs BOTH (1) Livo's platform builder key and (2) the @polymarket/client
321
+ // wrap which isn't wired yet. Message reflects which prerequisite is missing.
322
+ if (this.builderConfigured) {
323
+ throw new Error("Polymarket builder key is configured, but V2 order placement via @polymarket/client isn't wired up in " +
324
+ "this runtime yet (deposit-wallet deploy + EIP-1271 signing) — tracked in docs/POLYMARKET-V2-IMPLEMENTATION.md. " +
325
+ "Market-DATA reads work. { allowLegacyV1: true } enables the deprecated V1 path on testnet.");
326
+ }
327
+ throw new Error("Polymarket V2 trading needs Livo's platform builder key (env.POLYMARKET_BUILDER_API_KEY), which isn't " +
328
+ "configured. V2 (Apr 2026) requires the deposit-wallet flow — a plain EOA maker is rejected — and deploying " +
329
+ "a deposit wallet needs a Polymarket Builder/Relayer credential (one Livo-held key serves all projects; see " +
330
+ "docs/POLYMARKET-V2-IMPLEMENTATION.md). Market-DATA reads work without it. { allowLegacyV1: true } = V1 testnet.");
318
331
  }
319
332
  async deriveOrCreateApiKey() {
320
333
  // Try derive (deterministic) first; fall back to create.
@@ -68,6 +68,16 @@ describe("Polymarket V2 trading gate", () => {
68
68
  expect(new Polymarket({}).collateralToken).toBe("0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB");
69
69
  expect(new Polymarket({}, { allowLegacyV1: true }).collateralToken).toBeUndefined();
70
70
  });
71
+ it("detects the platform builder key and adjusts the gate message", async () => {
72
+ // No key configured → message points at the missing platform builder key.
73
+ const noKey = new Polymarket({ POLYMARKET_PRIVATE_KEY: PK });
74
+ expect(noKey.builderConfigured).toBe(false);
75
+ await expect(noKey.placeOrder({ tokenId: "1", side: "BUY", price: 0.5, size: 2, tickSize: "0.01", negRisk: false })).rejects.toThrow(/platform builder key/);
76
+ // Platform key injected (env) → detected; gate now points at the un-wired SDK path.
77
+ const withKey = new Polymarket({ POLYMARKET_PRIVATE_KEY: PK, POLYMARKET_BUILDER_API_KEY: "blder_test" });
78
+ expect(withKey.builderConfigured).toBe(true);
79
+ await expect(withKey.placeOrder({ tokenId: "1", side: "BUY", price: 0.5, size: 2, tickSize: "0.01", negRisk: false })).rejects.toThrow(/isn't wired up/);
80
+ });
71
81
  });
72
82
  describe("Polymarket data helpers", () => {
73
83
  it("resolveMarket maps Gamma clobTokenIds + outcomes to tradable tokens", async () => {
@@ -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
+ }
@@ -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>;