@livo-build/runtime 0.2.6 → 0.2.13
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/email.d.ts +74 -0
- package/dist/email.js +100 -0
- package/dist/email.test.d.ts +1 -0
- package/dist/email.test.js +68 -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/hyperliquidBridge.d.ts +30 -0
- package/dist/hyperliquidBridge.js +61 -0
- package/dist/index.d.ts +24 -1
- package/dist/index.js +17 -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/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";
|
|
@@ -24,11 +28,24 @@ export { Indexer, indexer, indexerEnvKey, GraphQLError } from "./indexer.js";
|
|
|
24
28
|
// Telegram — webhook verification + reply plumbing for bots.
|
|
25
29
|
export { Telegram } from "./telegram.js";
|
|
26
30
|
export { verifyTelegramInitData, verifyTelegramLoginWidget, parseStartPayload } from "./telegramAuth.js";
|
|
31
|
+
// Email — the project's own inbox at {slug}@livo.build (read + send/reply).
|
|
32
|
+
export { Email } from "./email.js";
|
|
27
33
|
export { TelegramLinks, TELEGRAM_LINKS_TABLE } from "./telegramLinks.js";
|
|
28
34
|
export { tokenBalanceOf, meetsGate } from "./gate.js";
|
|
35
|
+
export { tokenMetadata, ownerOf, tokenURI } from "./reads.js";
|
|
36
|
+
export { resolveUri, fetchTokenMetadata } from "./nft.js";
|
|
37
|
+
export { createSiweMessage, verifySiweMessage } from "./siwe.js";
|
|
38
|
+
export { createSession, verifySession } from "./sessions.js";
|
|
39
|
+
export { json, error, withCors, CORS_HEADERS, Router, sse } from "./http.js";
|
|
40
|
+
export { rateLimit } from "./ratelimit.js";
|
|
41
|
+
export { encrypt, decrypt } from "./aes.js";
|
|
42
|
+
export { cached } from "./cache.js";
|
|
43
|
+
export { verifyWebhook, signWebhook } from "./webhook.js";
|
|
29
44
|
// Hyperliquid — perps/spot data + trading (wraps @nktkas/hyperliquid; signs with
|
|
30
45
|
// the runtime's EIP-712 signer, no viem/ethers in the bundle).
|
|
31
46
|
export { Hyperliquid } from "./hyperliquid.js";
|
|
47
|
+
// Hyperliquid bridge — deposit USDC from Arbitrum into the L1 account (funds-in).
|
|
48
|
+
export { depositToHyperliquid, usdcBaseUnits, HL_BRIDGE } from "./hyperliquidBridge.js";
|
|
32
49
|
// Polymarket — prediction-market data (public, frontend-safe) + CLOB order
|
|
33
50
|
// placement (native EIP-712 + L2 HMAC; no @polymarket/clob-client, no ethers).
|
|
34
51
|
export { Polymarket } from "./polymarket.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
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
interface MinimalEnv {
|
|
2
|
+
[key: string]: unknown;
|
|
3
|
+
}
|
|
4
|
+
/** The public Livo signals engine. Override via `{ url }` or `SIGNALS_URL`. */
|
|
5
|
+
export declare const DEFAULT_SIGNALS_URL = "https://signals.livo.build";
|
|
6
|
+
export interface SignalsOptions {
|
|
7
|
+
/** Override the engine base URL (takes precedence over env). */
|
|
8
|
+
url?: string;
|
|
9
|
+
/** env key holding the engine base URL. Default `SIGNALS_URL`. */
|
|
10
|
+
urlSecret?: string;
|
|
11
|
+
}
|
|
12
|
+
/** A live, above-floor market the engine is metering. USD figures are dollars;
|
|
13
|
+
* `chg*` are percent; `spark` is recent candle closes (oldest→newest). */
|
|
14
|
+
export interface Market {
|
|
15
|
+
pair: string;
|
|
16
|
+
base: string;
|
|
17
|
+
quote: string;
|
|
18
|
+
pool: string;
|
|
19
|
+
price: number;
|
|
20
|
+
liq: number;
|
|
21
|
+
vol5m: number;
|
|
22
|
+
vol1h: number;
|
|
23
|
+
vol24h: number;
|
|
24
|
+
trades5m: number;
|
|
25
|
+
buyers: number;
|
|
26
|
+
chg5m: number;
|
|
27
|
+
chg1h: number;
|
|
28
|
+
chg24h: number;
|
|
29
|
+
spark: number[];
|
|
30
|
+
}
|
|
31
|
+
/** A fired signal-match (the public house feed: whale_buy/sell, price_surge, new_pool, …). */
|
|
32
|
+
export interface SignalMatch {
|
|
33
|
+
signal: string;
|
|
34
|
+
detail: string;
|
|
35
|
+
pair: string;
|
|
36
|
+
base: string;
|
|
37
|
+
pool: string;
|
|
38
|
+
side: string;
|
|
39
|
+
usd: number;
|
|
40
|
+
actor: string;
|
|
41
|
+
ago: number;
|
|
42
|
+
}
|
|
43
|
+
/** A recent priced swap from the firehose. */
|
|
44
|
+
export interface Swap {
|
|
45
|
+
block: number;
|
|
46
|
+
pair: string;
|
|
47
|
+
base: string;
|
|
48
|
+
pool: string;
|
|
49
|
+
side: string;
|
|
50
|
+
usd: number;
|
|
51
|
+
conf: string;
|
|
52
|
+
ago: number;
|
|
53
|
+
}
|
|
54
|
+
/** A newly-launched pool (young + climbing). `age` seconds, `pct` since launch. */
|
|
55
|
+
export interface Launch {
|
|
56
|
+
pair: string;
|
|
57
|
+
base: string;
|
|
58
|
+
pool: string;
|
|
59
|
+
age: number;
|
|
60
|
+
price: number;
|
|
61
|
+
pct: number;
|
|
62
|
+
vol: number;
|
|
63
|
+
buyers: number;
|
|
64
|
+
liq: number;
|
|
65
|
+
}
|
|
66
|
+
/** The full engine snapshot (the `/data` document). */
|
|
67
|
+
export interface EngineSnapshot {
|
|
68
|
+
mode: string;
|
|
69
|
+
caught_up: boolean;
|
|
70
|
+
head_block: number;
|
|
71
|
+
markets_tracked: number;
|
|
72
|
+
matches_total: number;
|
|
73
|
+
swaps_total: number;
|
|
74
|
+
markets: Market[];
|
|
75
|
+
matches: SignalMatch[];
|
|
76
|
+
swaps: Swap[];
|
|
77
|
+
launches: Launch[];
|
|
78
|
+
[key: string]: unknown;
|
|
79
|
+
}
|
|
80
|
+
export interface MarketFilter {
|
|
81
|
+
/** Min real TVL (USD). */
|
|
82
|
+
minLiq?: number;
|
|
83
|
+
/** Min 5-minute volume (USD) — "heating up right now". */
|
|
84
|
+
minVol5m?: number;
|
|
85
|
+
/** Min 24h volume (USD). */
|
|
86
|
+
minVol24h?: number;
|
|
87
|
+
/** Substring match on the pair, case-insensitive (e.g. "WETH"). */
|
|
88
|
+
pair?: string;
|
|
89
|
+
/** Cap the number returned (already sorted by 5m volume, desc). */
|
|
90
|
+
limit?: number;
|
|
91
|
+
}
|
|
92
|
+
export declare class Signals {
|
|
93
|
+
readonly url: string;
|
|
94
|
+
constructor(env?: MinimalEnv, options?: SignalsOptions);
|
|
95
|
+
/** The raw engine snapshot (markets + recent swaps + fired matches + launches). */
|
|
96
|
+
data(): Promise<EngineSnapshot>;
|
|
97
|
+
/** Live markets, newest-hottest first, optionally filtered. */
|
|
98
|
+
markets(filter?: MarketFilter): Promise<Market[]>;
|
|
99
|
+
/** One market by pool OR base-token address (0x-insensitive). undefined if absent. */
|
|
100
|
+
market(poolOrToken: string): Promise<Market | undefined>;
|
|
101
|
+
/** Find a token's deepest market by address or pair symbol (e.g. "PEPE"). */
|
|
102
|
+
token(addressOrSymbol: string): Promise<Market | undefined>;
|
|
103
|
+
/** Recently fired public signal-matches, newest first. */
|
|
104
|
+
matches(opts?: {
|
|
105
|
+
signal?: string;
|
|
106
|
+
limit?: number;
|
|
107
|
+
}): Promise<SignalMatch[]>;
|
|
108
|
+
/** Recent priced swaps (firehose), newest first. */
|
|
109
|
+
swaps(opts?: {
|
|
110
|
+
minUsd?: number;
|
|
111
|
+
side?: "BUY" | "SELL";
|
|
112
|
+
limit?: number;
|
|
113
|
+
}): Promise<Swap[]>;
|
|
114
|
+
/** Newly-launched pools (young + climbing). */
|
|
115
|
+
launches(): Promise<Launch[]>;
|
|
116
|
+
/** True if the engine is live and caught up to the chain tip. */
|
|
117
|
+
health(): Promise<boolean>;
|
|
118
|
+
/**
|
|
119
|
+
* Subscribe to the live Server-Sent-Events stream (for long-lived servers /
|
|
120
|
+
* Durable Objects). `onEvent` is called with each event's topic
|
|
121
|
+
* (`stats` | `swap` | `signal` | `pending`) and parsed JSON payload. Runs until
|
|
122
|
+
* the stream closes or `signal` (AbortSignal) aborts. Keepers should poll
|
|
123
|
+
* `markets()`/`matches()` on their schedule instead.
|
|
124
|
+
*/
|
|
125
|
+
stream(onEvent: (topic: string, data: unknown) => void | Promise<void>, opts?: {
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
}): Promise<void>;
|
|
128
|
+
}
|
|
129
|
+
/** Convenience: `await signals(env).markets({ minVol5m: 50_000 })`. */
|
|
130
|
+
export declare function signals(env?: MinimalEnv, options?: SignalsOptions): Signals;
|
|
131
|
+
export {};
|
package/dist/signals.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Signals — a read client for the Livo on-chain signals engine ("Signal Radar").
|
|
2
|
+
// The engine is a SHARED public service (mainnet DEX activity, priced + metered):
|
|
3
|
+
// live markets, recent swaps, and fired signal-matches. This is the PULL side —
|
|
4
|
+
// poll it from a keeper or read it from a server/bot. The PUSH side (get a webhook
|
|
5
|
+
// when a signal fires for YOUR token) is `defineWatcher` + the create_watcher tool.
|
|
6
|
+
//
|
|
7
|
+
// import { signals } from "@livo-build/runtime";
|
|
8
|
+
// const sig = signals(env); // zero-config → signals.livo.build
|
|
9
|
+
// const hot = await sig.markets({ minVol5m: 50_000 }); // markets heating up now
|
|
10
|
+
// const m = await sig.token("0x6982…"); // one token's market
|
|
11
|
+
// const fired = await sig.matches({ signal: "whale_buy" });
|
|
12
|
+
//
|
|
13
|
+
// Zero config: defaults to the public engine, so it works out of the box. Override
|
|
14
|
+
// with { url } or a SIGNALS_URL binding for a private/local engine.
|
|
15
|
+
/** The public Livo signals engine. Override via `{ url }` or `SIGNALS_URL`. */
|
|
16
|
+
export const DEFAULT_SIGNALS_URL = "https://signals.livo.build";
|
|
17
|
+
const bare = (a) => a.replace(/^0x/, "").toLowerCase();
|
|
18
|
+
export class Signals {
|
|
19
|
+
url;
|
|
20
|
+
constructor(env, options = {}) {
|
|
21
|
+
const fromEnv = env?.[options.urlSecret ?? "SIGNALS_URL"];
|
|
22
|
+
this.url = (options.url ?? fromEnv ?? DEFAULT_SIGNALS_URL).replace(/\/$/, "");
|
|
23
|
+
}
|
|
24
|
+
/** The raw engine snapshot (markets + recent swaps + fired matches + launches). */
|
|
25
|
+
async data() {
|
|
26
|
+
const res = await fetch(`${this.url}/data`, { headers: { accept: "application/json" } });
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
throw new Error(`signals engine /data failed: HTTP ${res.status}`);
|
|
29
|
+
}
|
|
30
|
+
return (await res.json());
|
|
31
|
+
}
|
|
32
|
+
/** Live markets, newest-hottest first, optionally filtered. */
|
|
33
|
+
async markets(filter = {}) {
|
|
34
|
+
let rows = (await this.data()).markets ?? [];
|
|
35
|
+
if (filter.minLiq != null)
|
|
36
|
+
rows = rows.filter((m) => m.liq >= filter.minLiq);
|
|
37
|
+
if (filter.minVol5m != null)
|
|
38
|
+
rows = rows.filter((m) => m.vol5m >= filter.minVol5m);
|
|
39
|
+
if (filter.minVol24h != null)
|
|
40
|
+
rows = rows.filter((m) => m.vol24h >= filter.minVol24h);
|
|
41
|
+
if (filter.pair) {
|
|
42
|
+
const q = filter.pair.toLowerCase();
|
|
43
|
+
rows = rows.filter((m) => m.pair.toLowerCase().includes(q));
|
|
44
|
+
}
|
|
45
|
+
return filter.limit != null ? rows.slice(0, filter.limit) : rows;
|
|
46
|
+
}
|
|
47
|
+
/** One market by pool OR base-token address (0x-insensitive). undefined if absent. */
|
|
48
|
+
async market(poolOrToken) {
|
|
49
|
+
const q = bare(poolOrToken);
|
|
50
|
+
return (await this.data()).markets?.find((m) => bare(m.pool) === q || bare(m.base) === q);
|
|
51
|
+
}
|
|
52
|
+
/** Find a token's deepest market by address or pair symbol (e.g. "PEPE"). */
|
|
53
|
+
async token(addressOrSymbol) {
|
|
54
|
+
const rows = (await this.data()).markets ?? [];
|
|
55
|
+
const q = addressOrSymbol.toLowerCase();
|
|
56
|
+
const byAddr = rows.find((m) => bare(m.base) === bare(addressOrSymbol));
|
|
57
|
+
if (byAddr)
|
|
58
|
+
return byAddr;
|
|
59
|
+
// Symbol match → the deepest (highest-liq) market for that base symbol.
|
|
60
|
+
const matches = rows.filter((m) => m.pair.toLowerCase().split("/")[0] === q);
|
|
61
|
+
return matches.sort((a, b) => b.liq - a.liq)[0];
|
|
62
|
+
}
|
|
63
|
+
/** Recently fired public signal-matches, newest first. */
|
|
64
|
+
async matches(opts = {}) {
|
|
65
|
+
let rows = (await this.data()).matches ?? [];
|
|
66
|
+
if (opts.signal)
|
|
67
|
+
rows = rows.filter((m) => m.signal === opts.signal);
|
|
68
|
+
return opts.limit != null ? rows.slice(0, opts.limit) : rows;
|
|
69
|
+
}
|
|
70
|
+
/** Recent priced swaps (firehose), newest first. */
|
|
71
|
+
async swaps(opts = {}) {
|
|
72
|
+
let rows = (await this.data()).swaps ?? [];
|
|
73
|
+
if (opts.minUsd != null)
|
|
74
|
+
rows = rows.filter((s) => s.usd >= opts.minUsd);
|
|
75
|
+
if (opts.side)
|
|
76
|
+
rows = rows.filter((s) => s.side === opts.side);
|
|
77
|
+
return opts.limit != null ? rows.slice(0, opts.limit) : rows;
|
|
78
|
+
}
|
|
79
|
+
/** Newly-launched pools (young + climbing). */
|
|
80
|
+
async launches() {
|
|
81
|
+
return (await this.data()).launches ?? [];
|
|
82
|
+
}
|
|
83
|
+
/** True if the engine is live and caught up to the chain tip. */
|
|
84
|
+
async health() {
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch(`${this.url}/health`);
|
|
87
|
+
return res.ok;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Subscribe to the live Server-Sent-Events stream (for long-lived servers /
|
|
95
|
+
* Durable Objects). `onEvent` is called with each event's topic
|
|
96
|
+
* (`stats` | `swap` | `signal` | `pending`) and parsed JSON payload. Runs until
|
|
97
|
+
* the stream closes or `signal` (AbortSignal) aborts. Keepers should poll
|
|
98
|
+
* `markets()`/`matches()` on their schedule instead.
|
|
99
|
+
*/
|
|
100
|
+
async stream(onEvent, opts = {}) {
|
|
101
|
+
const res = await fetch(`${this.url}/stream`, {
|
|
102
|
+
headers: { accept: "text/event-stream" },
|
|
103
|
+
signal: opts.signal,
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok || !res.body) {
|
|
106
|
+
throw new Error(`signals engine /stream failed: HTTP ${res.status}`);
|
|
107
|
+
}
|
|
108
|
+
const reader = res.body.getReader();
|
|
109
|
+
const decoder = new TextDecoder();
|
|
110
|
+
let buf = "";
|
|
111
|
+
for (;;) {
|
|
112
|
+
const { value, done } = await reader.read();
|
|
113
|
+
if (done)
|
|
114
|
+
break;
|
|
115
|
+
buf += decoder.decode(value, { stream: true });
|
|
116
|
+
// SSE frames are separated by a blank line; each has `event:` + `data:` lines.
|
|
117
|
+
let sep;
|
|
118
|
+
while ((sep = buf.indexOf("\n\n")) !== -1) {
|
|
119
|
+
const frame = buf.slice(0, sep);
|
|
120
|
+
buf = buf.slice(sep + 2);
|
|
121
|
+
let topic = "message";
|
|
122
|
+
const dataLines = [];
|
|
123
|
+
for (const line of frame.split("\n")) {
|
|
124
|
+
if (line.startsWith("event:"))
|
|
125
|
+
topic = line.slice(6).trim();
|
|
126
|
+
else if (line.startsWith("data:"))
|
|
127
|
+
dataLines.push(line.slice(5).trim());
|
|
128
|
+
}
|
|
129
|
+
if (!dataLines.length)
|
|
130
|
+
continue;
|
|
131
|
+
let payload = dataLines.join("\n");
|
|
132
|
+
try {
|
|
133
|
+
payload = JSON.parse(payload);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* leave as string */
|
|
137
|
+
}
|
|
138
|
+
await onEvent(topic, payload);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Convenience: `await signals(env).markets({ minVol5m: 50_000 })`. */
|
|
144
|
+
export function signals(env, options) {
|
|
145
|
+
return new Signals(env, options);
|
|
146
|
+
}
|
package/dist/siwe.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface SiweParams {
|
|
2
|
+
/** The domain requesting the sign-in (e.g. "app.livo.build"). */
|
|
3
|
+
domain: string;
|
|
4
|
+
address: string;
|
|
5
|
+
uri: string;
|
|
6
|
+
chainId: number;
|
|
7
|
+
nonce: string;
|
|
8
|
+
statement?: string;
|
|
9
|
+
/** ISO timestamp; defaults to now. */
|
|
10
|
+
issuedAt?: string;
|
|
11
|
+
/** ISO timestamp; if set, verifySiweMessage rejects after it. */
|
|
12
|
+
expirationTime?: string;
|
|
13
|
+
version?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function createSiweMessage(p: SiweParams): string;
|
|
16
|
+
export interface SiweVerifyResult {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
address?: string;
|
|
19
|
+
nonce?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function verifySiweMessage(message: string, signature: string, opts?: {
|
|
22
|
+
nonce?: string;
|
|
23
|
+
domain?: string;
|
|
24
|
+
}): Promise<SiweVerifyResult>;
|