@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.
- 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/gate.d.ts +10 -0
- package/dist/gate.js +18 -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 +27 -2
- package/dist/index.js +16 -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/polymarket.d.ts +12 -0
- package/dist/polymarket.js +19 -6
- package/dist/polymarket.test.js +10 -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/telegram.d.ts +31 -0
- package/dist/telegram.js +73 -0
- package/dist/telegramAuth.d.ts +16 -0
- package/dist/telegramAuth.js +108 -0
- package/dist/telegramAuth.test.d.ts +1 -0
- package/dist/telegramAuth.test.js +68 -0
- package/dist/telegramLinks.d.ts +27 -0
- package/dist/telegramLinks.js +78 -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 +14 -4
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>;
|
package/dist/siwe.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Sign-In With Ethereum (EIP-4361) — build the canonical message + verify a signed
|
|
2
|
+
// one. The standard way to authenticate a wallet to a backend; pairs with sessions.
|
|
3
|
+
import { verifyMessage } from "./sig.js";
|
|
4
|
+
export function createSiweMessage(p) {
|
|
5
|
+
const lines = [`${p.domain} wants you to sign in with your Ethereum account:`, p.address, ""];
|
|
6
|
+
if (p.statement)
|
|
7
|
+
lines.push(p.statement, "");
|
|
8
|
+
lines.push(`URI: ${p.uri}`, `Version: ${p.version ?? "1"}`, `Chain ID: ${p.chainId}`, `Nonce: ${p.nonce}`, `Issued At: ${p.issuedAt ?? new Date().toISOString()}`);
|
|
9
|
+
if (p.expirationTime)
|
|
10
|
+
lines.push(`Expiration Time: ${p.expirationTime}`);
|
|
11
|
+
return lines.join("\n");
|
|
12
|
+
}
|
|
13
|
+
function field(message, label) {
|
|
14
|
+
const m = new RegExp(`^${label}(.*)$`, "m").exec(message);
|
|
15
|
+
return m?.[1]?.trim();
|
|
16
|
+
}
|
|
17
|
+
// Verify a SIWE message + signature: the signature recovers to the claimed address,
|
|
18
|
+
// the nonce matches (if given), and it isn't expired.
|
|
19
|
+
export async function verifySiweMessage(message, signature, opts = {}) {
|
|
20
|
+
const address = message.split("\n")[1]?.trim();
|
|
21
|
+
if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address))
|
|
22
|
+
return { valid: false };
|
|
23
|
+
const nonce = field(message, "Nonce: ");
|
|
24
|
+
if (opts.nonce && nonce !== opts.nonce)
|
|
25
|
+
return { valid: false };
|
|
26
|
+
if (opts.domain && !message.startsWith(opts.domain + " "))
|
|
27
|
+
return { valid: false };
|
|
28
|
+
const exp = field(message, "Expiration Time: ");
|
|
29
|
+
if (exp && Date.parse(exp) < Date.now())
|
|
30
|
+
return { valid: false };
|
|
31
|
+
const ok = await verifyMessage({ address: address, message, signature: signature });
|
|
32
|
+
return { valid: ok, address: ok ? address : undefined, nonce };
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/sse.test.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { sse } from "./http.js";
|
|
3
|
+
async function readAll(res) {
|
|
4
|
+
return await res.text();
|
|
5
|
+
}
|
|
6
|
+
describe("sse", () => {
|
|
7
|
+
it("formats events (data/event/id) and a heartbeat, and sets the right headers", async () => {
|
|
8
|
+
const conn = sse();
|
|
9
|
+
expect(conn.response.headers.get("content-type")).toBe("text/event-stream");
|
|
10
|
+
conn.send({ px: 100 });
|
|
11
|
+
conn.send("hello", { event: "tick", id: "7" });
|
|
12
|
+
conn.ping();
|
|
13
|
+
conn.send("a\nb"); // multiline → two data: lines
|
|
14
|
+
conn.close();
|
|
15
|
+
const text = await readAll(conn.response);
|
|
16
|
+
expect(text).toContain('data: {"px":100}\n\n');
|
|
17
|
+
expect(text).toContain("event: tick\nid: 7\ndata: hello\n\n");
|
|
18
|
+
expect(text).toContain(": ping\n\n");
|
|
19
|
+
expect(text).toContain("data: a\ndata: b\n\n");
|
|
20
|
+
expect(conn.closed).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it("stops writing after close (no throw)", async () => {
|
|
23
|
+
const conn = sse();
|
|
24
|
+
conn.close();
|
|
25
|
+
conn.send({ ignored: true });
|
|
26
|
+
expect(await readAll(conn.response)).toBe("");
|
|
27
|
+
});
|
|
28
|
+
});
|
package/dist/telegram.d.ts
CHANGED
|
@@ -67,5 +67,36 @@ export declare class Telegram {
|
|
|
67
67
|
command: string;
|
|
68
68
|
description: string;
|
|
69
69
|
}>): Promise<void>;
|
|
70
|
+
private api;
|
|
71
|
+
/** Parse a `chat_join_request` update (a user asking to join a join-request chat). */
|
|
72
|
+
joinRequest(update: Record<string, unknown>): ChatJoinRequest | null;
|
|
73
|
+
/** Admit a pending join request. */
|
|
74
|
+
approveJoinRequest(chatId: number | string, userId: number): Promise<unknown>;
|
|
75
|
+
/** Reject a pending join request. */
|
|
76
|
+
declineJoinRequest(chatId: number | string, userId: number): Promise<unknown>;
|
|
77
|
+
/** Remove (ban) a member — the "boot" when they no longer qualify. */
|
|
78
|
+
banMember(chatId: number | string, userId: number, opts?: {
|
|
79
|
+
untilDate?: number;
|
|
80
|
+
revokeMessages?: boolean;
|
|
81
|
+
}): Promise<unknown>;
|
|
82
|
+
/** Lift a ban so a re-qualified user can rejoin (does NOT add them back). */
|
|
83
|
+
unbanMember(chatId: number | string, userId: number, opts?: {
|
|
84
|
+
onlyIfBanned?: boolean;
|
|
85
|
+
}): Promise<unknown>;
|
|
86
|
+
/** Create an invite link. With createsJoinRequest, joins need bot approval (the gate). */
|
|
87
|
+
createInviteLink(chatId: number | string, opts?: {
|
|
88
|
+
name?: string;
|
|
89
|
+
createsJoinRequest?: boolean;
|
|
90
|
+
expireDate?: number;
|
|
91
|
+
}): Promise<string>;
|
|
92
|
+
/** A member's status in a chat ("member" | "left" | "kicked" | "administrator" | "creator" | "restricted"). */
|
|
93
|
+
memberStatus(chatId: number | string, userId: number): Promise<string | null>;
|
|
94
|
+
}
|
|
95
|
+
export interface ChatJoinRequest {
|
|
96
|
+
chatId: number;
|
|
97
|
+
userId: number;
|
|
98
|
+
username?: string;
|
|
99
|
+
from: Record<string, unknown>;
|
|
100
|
+
raw: Record<string, unknown>;
|
|
70
101
|
}
|
|
71
102
|
export {};
|
package/dist/telegram.js
CHANGED
|
@@ -132,4 +132,77 @@ export class Telegram {
|
|
|
132
132
|
body: JSON.stringify({ commands }),
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
|
+
// ---- Group / membership management (token-gated rooms) --------------------
|
|
136
|
+
// Generic Bot API call: POST JSON, return result, throw on Telegram error.
|
|
137
|
+
async api(method, body) {
|
|
138
|
+
if (!this.token)
|
|
139
|
+
throw new Error("Telegram: no bot token (set_secret TELEGRAM_BOT_TOKEN=<token>).");
|
|
140
|
+
const res = await fetch(`${API}${this.token}/${method}`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: { "content-type": "application/json" },
|
|
143
|
+
body: JSON.stringify(body),
|
|
144
|
+
});
|
|
145
|
+
const j = (await res.json().catch(() => null));
|
|
146
|
+
if (!res.ok || !j?.ok)
|
|
147
|
+
throw new Error(`Telegram ${method} failed: ${j?.description ?? "HTTP " + res.status}`);
|
|
148
|
+
return j.result;
|
|
149
|
+
}
|
|
150
|
+
/** Parse a `chat_join_request` update (a user asking to join a join-request chat). */
|
|
151
|
+
joinRequest(update) {
|
|
152
|
+
const r = update.chat_join_request;
|
|
153
|
+
if (!r)
|
|
154
|
+
return null;
|
|
155
|
+
const from = r.from;
|
|
156
|
+
const chat = r.chat;
|
|
157
|
+
if (!from || !chat)
|
|
158
|
+
return null;
|
|
159
|
+
return {
|
|
160
|
+
chatId: chat.id,
|
|
161
|
+
userId: from.id,
|
|
162
|
+
username: from.username,
|
|
163
|
+
from,
|
|
164
|
+
raw: r,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Admit a pending join request. */
|
|
168
|
+
approveJoinRequest(chatId, userId) {
|
|
169
|
+
return this.api("approveChatJoinRequest", { chat_id: chatId, user_id: userId });
|
|
170
|
+
}
|
|
171
|
+
/** Reject a pending join request. */
|
|
172
|
+
declineJoinRequest(chatId, userId) {
|
|
173
|
+
return this.api("declineChatJoinRequest", { chat_id: chatId, user_id: userId });
|
|
174
|
+
}
|
|
175
|
+
/** Remove (ban) a member — the "boot" when they no longer qualify. */
|
|
176
|
+
banMember(chatId, userId, opts = {}) {
|
|
177
|
+
return this.api("banChatMember", {
|
|
178
|
+
chat_id: chatId,
|
|
179
|
+
user_id: userId,
|
|
180
|
+
...(opts.untilDate ? { until_date: opts.untilDate } : {}),
|
|
181
|
+
...(opts.revokeMessages ? { revoke_messages: true } : {}),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** Lift a ban so a re-qualified user can rejoin (does NOT add them back). */
|
|
185
|
+
unbanMember(chatId, userId, opts = { onlyIfBanned: true }) {
|
|
186
|
+
return this.api("unbanChatMember", { chat_id: chatId, user_id: userId, only_if_banned: opts.onlyIfBanned !== false });
|
|
187
|
+
}
|
|
188
|
+
/** Create an invite link. With createsJoinRequest, joins need bot approval (the gate). */
|
|
189
|
+
async createInviteLink(chatId, opts = {}) {
|
|
190
|
+
const result = (await this.api("createChatInviteLink", {
|
|
191
|
+
chat_id: chatId,
|
|
192
|
+
...(opts.name ? { name: opts.name } : {}),
|
|
193
|
+
...(opts.createsJoinRequest ? { creates_join_request: true } : {}),
|
|
194
|
+
...(opts.expireDate ? { expire_date: opts.expireDate } : {}),
|
|
195
|
+
}));
|
|
196
|
+
return result.invite_link ?? "";
|
|
197
|
+
}
|
|
198
|
+
/** A member's status in a chat ("member" | "left" | "kicked" | "administrator" | "creator" | "restricted"). */
|
|
199
|
+
async memberStatus(chatId, userId) {
|
|
200
|
+
try {
|
|
201
|
+
const result = (await this.api("getChatMember", { chat_id: chatId, user_id: userId }));
|
|
202
|
+
return result.status ?? null;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
135
208
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TelegramUser {
|
|
2
|
+
id: number;
|
|
3
|
+
username?: string;
|
|
4
|
+
firstName?: string;
|
|
5
|
+
lastName?: string;
|
|
6
|
+
languageCode?: string;
|
|
7
|
+
isPremium?: boolean;
|
|
8
|
+
photoUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface VerifyOptions {
|
|
11
|
+
/** Reject if auth_date is older than this many seconds (0 = no check). Default 86400. */
|
|
12
|
+
maxAgeSeconds?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function verifyTelegramInitData(initData: string, botToken: string, options?: VerifyOptions): Promise<TelegramUser | null>;
|
|
15
|
+
export declare function verifyTelegramLoginWidget(data: Record<string, string | number | undefined>, botToken: string, options?: VerifyOptions): Promise<TelegramUser | null>;
|
|
16
|
+
export declare function parseStartPayload(text: string): string | null;
|