@livo-build/runtime 0.2.1 → 0.2.3

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/wallet.js ADDED
@@ -0,0 +1,122 @@
1
+ // Wallet — per-user custodial wallets for bots. A bot's END USERS each get their
2
+ // own wallet(s); the bot signs on a user's behalf WITHOUT ever holding raw key
3
+ // material. Every management + signing call goes to the platform (WALLET_API_URL,
4
+ // authorized by WALLET_API_TOKEN, both injected at deploy); Convex resolves the
5
+ // user's active wallet and signs server-side, so a leaked Worker never leaks keys.
6
+ //
7
+ // const w = new Wallet(env);
8
+ // const acct = w.user(msg.from.id); // a handle scoped to one end-user
9
+ // await acct.address(); // their active wallet (auto-created)
10
+ // await acct.list(); // every wallet they own
11
+ // await acct.import("0x<key>", "trading"); // bring your own key
12
+ // await acct.use("trading"); // switch the active wallet
13
+ // const hash = await acct.send({ address, abi, functionName, args }); // platform signs
14
+ //
15
+ // Reads (balance, receipts) use the injected RPC_URL via a plain Chain — no key.
16
+ import { Chain } from "./chain.js";
17
+ import { encodeFunctionData, findFunction } from "./abi.js";
18
+ /** A handle bound to a single end-user (e.g. one Telegram account). */
19
+ export class UserWallet {
20
+ wallet;
21
+ userKey;
22
+ constructor(wallet,
23
+ /** Platform-namespaced user id, e.g. "tg:12345". */
24
+ userKey) {
25
+ this.wallet = wallet;
26
+ this.userKey = userKey;
27
+ }
28
+ /** Every wallet the user owns (auto-creates their default on first call). */
29
+ async list() {
30
+ const r = await this.wallet.post("accounts", { op: "list", user: this.userKey });
31
+ return (r.wallets ?? []);
32
+ }
33
+ /** The user's active wallet (the one `send` signs with), auto-created if none. */
34
+ async account() {
35
+ const r = await this.wallet.post("accounts", { op: "address", user: this.userKey });
36
+ return r.wallet;
37
+ }
38
+ /** Shorthand for the active wallet's address. */
39
+ async address() {
40
+ return (await this.account()).address;
41
+ }
42
+ /** Generate a new HD wallet (from the user's own seed) and make it active. */
43
+ async create(label) {
44
+ const r = await this.wallet.post("accounts", { op: "create", user: this.userKey, label });
45
+ return r.wallet;
46
+ }
47
+ /** Import an external private key and make it active. */
48
+ async import(privateKey, label) {
49
+ const r = await this.wallet.post("accounts", { op: "import", user: this.userKey, privateKey, label });
50
+ return r.wallet;
51
+ }
52
+ /** Switch the active wallet by label. */
53
+ async use(label) {
54
+ const r = await this.wallet.post("accounts", { op: "use", user: this.userKey, label });
55
+ return r.wallet;
56
+ }
57
+ /** Stop tracking a wallet by label (active reassigns to another if needed). */
58
+ async remove(label) {
59
+ await this.wallet.post("accounts", { op: "remove", user: this.userKey, label });
60
+ }
61
+ /** Native balance (wei) of the user's active wallet. Reads via RPC, no key. */
62
+ async balance() {
63
+ return this.wallet.chain().getBalance(await this.address());
64
+ }
65
+ /**
66
+ * Sign + broadcast a state-changing call with the user's ACTIVE wallet. The
67
+ * calldata is encoded here; the platform signs server-side and returns the hash.
68
+ */
69
+ async send(opts) {
70
+ const data = encodeFunctionData(findFunction(opts.abi, opts.functionName), opts.args ?? []);
71
+ const r = await this.wallet.post("sign", {
72
+ user: this.userKey,
73
+ to: opts.address,
74
+ data,
75
+ value: opts.value !== undefined ? `0x${opts.value.toString(16)}` : undefined,
76
+ });
77
+ return r.hash;
78
+ }
79
+ }
80
+ export class Wallet {
81
+ apiUrl;
82
+ apiToken;
83
+ env;
84
+ _chain;
85
+ constructor(env, options = {}) {
86
+ this.env = env;
87
+ this.apiUrl = (options.apiUrl ?? env?.WALLET_API_URL)?.replace(/\/$/, "");
88
+ this.apiToken = options.apiToken ?? env?.WALLET_API_TOKEN;
89
+ }
90
+ /** A handle for one end-user. Pass any stable id; it's namespaced as "tg:<id>". */
91
+ user(id, platform = "tg") {
92
+ const raw = String(id);
93
+ const userKey = /^[a-z]+:/.test(raw) ? raw : `${platform}:${raw}`;
94
+ return new UserWallet(this, userKey);
95
+ }
96
+ /** Lazily build a read-only Chain (for balances/receipts) from env RPC_URL. */
97
+ chain() {
98
+ if (!this._chain)
99
+ this._chain = new Chain(this.env);
100
+ return this._chain;
101
+ }
102
+ /** Wait for a tx broadcast by `send` to mine. Reads via RPC, no key. */
103
+ waitForReceipt(hash, opts) {
104
+ return this.chain().waitForReceipt(hash, opts);
105
+ }
106
+ /** Internal: POST to a wallet API sub-path with the bot bearer; throws on error. */
107
+ async post(path, body) {
108
+ if (!this.apiUrl || !this.apiToken) {
109
+ throw new Error("Wallet: not provisioned — WALLET_API_URL/WALLET_API_TOKEN missing (redeploy the bot so the platform injects them).");
110
+ }
111
+ const res = await fetch(`${this.apiUrl}/${path}`, {
112
+ method: "POST",
113
+ headers: { "content-type": "application/json", authorization: `Bearer ${this.apiToken}` },
114
+ body: JSON.stringify(body),
115
+ });
116
+ const j = (await res.json().catch(() => ({})));
117
+ if (!res.ok || j.error) {
118
+ throw new Error(`Wallet ${path}: ${String(j.message ?? j.error ?? `HTTP ${res.status}`)}`);
119
+ }
120
+ return j;
121
+ }
122
+ }
@@ -0,0 +1,44 @@
1
+ import type { Chain } from "./chain.js";
2
+ import type { Store } from "./store.js";
3
+ import type { AbiEvent, DecodedLog } from "./events.js";
4
+ import type { Hex } from "./hex.js";
5
+ export interface WatchLogsOptions {
6
+ /** Event to decode — an AbiEvent or a human-readable signature. */
7
+ event: AbiEvent | string;
8
+ /** Contract address(es) to filter (omit to match any). */
9
+ address?: Hex | Hex[];
10
+ /** Indexed-arg filter, positional or by name (see Chain.getLogs). */
11
+ args?: Record<string, unknown> | unknown[];
12
+ /** Store key holding this watcher's cursor. Namespace it per watcher. */
13
+ cursorKey: string;
14
+ /** First block to scan when no cursor exists yet. Default: current head (new logs only). */
15
+ fromBlock?: bigint;
16
+ /** Stay this many blocks behind head for reorg safety. Default 0. */
17
+ confirmations?: bigint;
18
+ /** Max blocks per eth_getLogs window (RPC range cap). Default 2000n. */
19
+ chunkSize?: bigint;
20
+ /** Invoked per non-empty chunk with that chunk's decoded logs. Must be idempotent. */
21
+ onLogs: (logs: DecodedLog[], range: {
22
+ fromBlock: bigint;
23
+ toBlock: bigint;
24
+ }) => Promise<void> | void;
25
+ }
26
+ export interface WatchLogsResult {
27
+ /** First block scanned this run. */
28
+ fromBlock: bigint;
29
+ /** New cursor — the last block scanned (head - confirmations). */
30
+ toBlock: bigint;
31
+ /** Total logs delivered to onLogs this run. */
32
+ logCount: number;
33
+ /** Number of eth_getLogs windows scanned. */
34
+ chunks: number;
35
+ }
36
+ /**
37
+ * Catch up on event logs since the last run. Reads the cursor (or `fromBlock`),
38
+ * scans `[cursor, head - confirmations]` in `chunkSize` windows, calls `onLogs`
39
+ * per non-empty chunk, and persists the cursor AFTER each chunk — so a Worker
40
+ * killed mid-catch-up resumes from the last completed window. Delivery is
41
+ * at-least-once: if `onLogs` throws, the cursor is not advanced past that chunk
42
+ * and the next tick retries it, so `onLogs` must be idempotent.
43
+ */
44
+ export declare function watchLogs(chain: Chain, store: Store, opts: WatchLogsOptions): Promise<WatchLogsResult>;
package/dist/watch.js ADDED
@@ -0,0 +1,46 @@
1
+ // watchLogs — resumable event catch-up for keepers. A Worker has no long-lived
2
+ // process, so this is NOT a websocket subscription: each scheduled tick reads new
3
+ // logs since a Store-persisted block cursor, up to the (confirmed) head, in
4
+ // chunked eth_getLogs windows. Layered as a free function over Chain + Store so
5
+ // Chain (Layer 1, may hold keys) never imports Store (Layer 3).
6
+ /**
7
+ * Catch up on event logs since the last run. Reads the cursor (or `fromBlock`),
8
+ * scans `[cursor, head - confirmations]` in `chunkSize` windows, calls `onLogs`
9
+ * per non-empty chunk, and persists the cursor AFTER each chunk — so a Worker
10
+ * killed mid-catch-up resumes from the last completed window. Delivery is
11
+ * at-least-once: if `onLogs` throws, the cursor is not advanced past that chunk
12
+ * and the next tick retries it, so `onLogs` must be idempotent.
13
+ */
14
+ export async function watchLogs(chain, store, opts) {
15
+ const confirmations = opts.confirmations ?? 0n;
16
+ const chunkSize = opts.chunkSize ?? 2000n;
17
+ const head = await chain.getBlockNumber();
18
+ const safeHead = head - confirmations;
19
+ const stored = await store.getJSON(opts.cursorKey);
20
+ const from = stored ? BigInt(stored.next) : (opts.fromBlock ?? safeHead);
21
+ if (from > safeHead) {
22
+ return { fromBlock: from, toBlock: from - 1n, logCount: 0, chunks: 0 };
23
+ }
24
+ let cur = from;
25
+ let logCount = 0;
26
+ let chunks = 0;
27
+ while (cur <= safeHead) {
28
+ const to = cur + chunkSize - 1n < safeHead ? cur + chunkSize - 1n : safeHead;
29
+ const logs = await chain.getLogs({
30
+ event: opts.event,
31
+ address: opts.address,
32
+ args: opts.args,
33
+ fromBlock: cur,
34
+ toBlock: to,
35
+ });
36
+ chunks++;
37
+ if (logs.length) {
38
+ await opts.onLogs(logs, { fromBlock: cur, toBlock: to });
39
+ logCount += logs.length;
40
+ }
41
+ // Advance + persist only after a successful onLogs, so a throw retries this window.
42
+ await store.setJSON(opts.cursorKey, { next: (to + 1n).toString() });
43
+ cur = to + 1n;
44
+ }
45
+ return { fromBlock: from, toBlock: safeHead, logCount, chunks };
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livo-build/runtime",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Livo runtime — chain signing/reads, D1 state, and logging for keepers, servers, and bots.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",