@livo-build/runtime 0.2.1 → 0.2.2

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/README.md CHANGED
@@ -1,13 +1,20 @@
1
1
  # @livo-build/runtime
2
2
 
3
- The standard library available to every Livo compute target that isn't a contract
4
- or a static frontend — **keepers** (cron Workers), **servers** (Durable Objects /
5
- long-running Workers), and **bots** (Telegram/Twitter Workers).
3
+ The standard library for Livo compute targets that bundle npm deps at deploy —
4
+ **keepers** (cron Workers) and **bots** (Telegram/Twitter Workers). (Servers /
5
+ Durable Objects deploy as a single un-bundled module today, so they can't import
6
+ this yet — use fetch + Web Crypto there; their `INDEXER_<NAME>_URL` env binding is
7
+ still injected.)
6
8
 
7
9
  It deletes the most-repeated, most-error-prone code these targets hand-roll:
8
- signing and sending on-chain transactions, reading contract state, talking to D1,
9
- and basic logging/retry. No more hand-bundling secp256k1 to fit a single Worker
10
- module.
10
+ signing and sending on-chain transactions, reading contract state, querying the
11
+ project's subgraph, the Telegram webhook dance, talking to D1, and basic
12
+ logging/retry. No more hand-bundling secp256k1 to fit a single Worker module.
13
+
14
+ At a glance: `Chain` (read/sign/send + RPC failover), `Relayer` (managed bot
15
+ signing), `bindContracts` (call contracts by name), `Indexer` (typed subgraph
16
+ queries), `Telegram` (webhook plumbing in one call), `Store` (D1 KV + idempotency),
17
+ `requireSecret`, `queue`, `retry`, `log`.
11
18
 
12
19
  ```ts
13
20
  import { Chain, Store, log } from "@livo-build/runtime";
@@ -39,7 +46,7 @@ keeper/bot/server that imports it ships a `package.json` declaring the dependenc
39
46
  audited `@noble` crypto — into one self-contained Worker module. No hand-bundling.
40
47
  - Targets with **no `package.json`** deploy exactly as before — the build step is
41
48
  opt-in by the presence of a manifest. Existing keepers keep working untouched.
42
- - Versions pin in `package.json` (`"@livo-build/runtime": "^0.1.0"`), so a runtime
49
+ - Versions pin in `package.json` (`"@livo-build/runtime": "^0.2.2"`), so a runtime
43
50
  update can't silently change a working keeper.
44
51
 
45
52
  Scaffold the common path with `create_keeper --template runtime-keeper`.
@@ -65,6 +72,18 @@ chain.address; // signer address (null when read-only)
65
72
  const hash = await chain.send({ address, abi, functionName, args });
66
73
  const receipt = await chain.waitForReceipt(hash, { timeoutMs, pollMs });
67
74
 
75
+ // reliable write — nonce resolved at broadcast, retry-on-collision + replace-by-fee
76
+ const h = await chain.sendReliable({ address, abi, functionName, args, confirmMs: 30_000 });
77
+
78
+ // batch reads — ONE eth_call via Multicall3 (allowFailure defaults true → null on revert)
79
+ const [supply, mine] = await chain.multicall([
80
+ { address: token, abi, functionName: "totalSupply" },
81
+ { address: token, abi, functionName: "balanceOf", args: [chain.address] },
82
+ ]);
83
+
84
+ // deploy a contract — to:null creation, waits for the receipt's contractAddress
85
+ const { address: deployed } = await chain.deploy({ abi, bytecode, args: [param] });
86
+
68
87
  // escape hatches
69
88
  chain.encodeFunction(abi, fn, args); // 0x calldata
70
89
  chain.signTx(txFields); // raw 0x EIP-1559 tx
@@ -101,6 +120,47 @@ const hash = await forum.write.createPost(topicId, body);
101
120
  container without the keeper's `node_modules`. `@livo-build/runtime/contracts`
102
121
  exports `bindContracts` and the binding types.)
103
122
 
123
+ ### RPC failover
124
+
125
+ `Chain` accepts several endpoints and rotates past transport failures (network
126
+ error, 5xx, 429) — a deterministic chain error (revert/nonce) is surfaced, not
127
+ retried elsewhere:
128
+
129
+ ```ts
130
+ new Chain(env, { rpcUrls: ["https://a", "https://b"] });
131
+ new Chain(env); // or set RPC_URL to a comma/whitespace-separated list
132
+ ```
133
+
134
+ ## Indexer — query the project's subgraph
135
+
136
+ `INDEXER_<NAME>_URL` is injected at deploy as the subgraph's **stable `live`
137
+ alias**, so the worker never holds a version-pinned URL that breaks on the next
138
+ `sync_indexers`/`reindex`:
139
+
140
+ ```ts
141
+ import { indexer } from "@livo-build/runtime";
142
+ const { keepers } = await indexer(env, "hearth").query(
143
+ `query($m:Int!){ keepers(where:{count_gte:$m}){ id count } }`, { m: 3 });
144
+ await indexer(env, "hearth").meta(); // { block, hasIndexingErrors }
145
+ ```
146
+
147
+ ## Telegram — webhook plumbing in one call
148
+
149
+ ```ts
150
+ import { Telegram } from "@livo-build/runtime";
151
+ export default {
152
+ fetch(req, env) {
153
+ return new Telegram(env).handleUpdate(req, (msg) =>
154
+ msg.text === "/start" ? "👋 hi " + msg.handle : "you said " + msg.text);
155
+ },
156
+ };
157
+ ```
158
+
159
+ `handleUpdate` verifies the `WEBHOOK_SECRET` secret-token, short-circuits Livo's
160
+ `__livo_test` harness (returns the computed reply without calling Telegram),
161
+ parses the update, and sends the reply. Lower-level `verify` / `parse` /
162
+ `sendMessage` / `setMyCommands` are exposed too.
163
+
104
164
  ## Layer 3 — State + utilities
105
165
 
106
166
  ```ts
@@ -110,10 +170,15 @@ await store.set("heartbeat", "3");
110
170
  await store.getJSON<Config>("config");
111
171
  await store.setJSON("config", obj);
112
172
 
173
+ // Idempotency: true only the FIRST time a key is seen — a retried cron tick or a
174
+ // redelivered webhook won't double-fire.
175
+ if (!(await store.once(`block:${n}`))) return;
176
+
113
177
  log.info("posted", { tx, topicId }); // structured, consistently prefixed
114
178
  log.error("send failed", err);
115
179
 
116
- import { retry } from "@livo-build/runtime";
180
+ import { requireSecret, retry } from "@livo-build/runtime";
181
+ const apiKey = requireSecret(env, "SOME_API_KEY"); // loud error if missing, not silent undefined
117
182
  await retry(() => chain.send(...), { tries: 3, backoffMs: 500 }); // flaky RPCs
118
183
  ```
119
184
 
@@ -121,6 +186,55 @@ await retry(() => chain.send(...), { tries: 3, backoffMs: 500 }); // flaky RPCs
121
186
  is `livo_kv(k TEXT PRIMARY KEY, v TEXT, updated_at INTEGER)` — stable and
122
187
  documented.
123
188
 
189
+ ## Watching events — `watchLogs`
190
+
191
+ A Worker has no long-lived process, so this is **catch-up per scheduled tick**, not
192
+ a websocket subscription. It reads new logs since a `Store`-persisted block cursor,
193
+ up to the (confirmed) head, in chunked `eth_getLogs` windows:
194
+
195
+ ```ts
196
+ import { watchLogs } from "@livo-build/runtime";
197
+
198
+ await watchLogs(chain, store, {
199
+ event: "Transfer(address indexed from, address indexed to, uint256 value)",
200
+ address: token, // optional filter
201
+ cursorKey: "transfers", // namespaced Store cursor
202
+ confirmations: 2n, // reorg safety (default 0)
203
+ chunkSize: 2000n, // match your RPC's eth_getLogs range cap
204
+ onLogs: async (logs, range) => { /* idempotent! at-least-once delivery */ },
205
+ });
206
+ ```
207
+
208
+ The cursor advances **after** each successful `onLogs`, so a throw retries that
209
+ window on the next tick — `onLogs` must be idempotent. First run defaults to "from
210
+ head" (new logs only); pass `fromBlock` to backfill.
211
+
212
+ ## Bots — `Relayer`
213
+
214
+ `Relayer extends Chain`: same read/write/multicall surface, but it signs with the
215
+ custodied **`RELAYER_PRIVATE_KEY`** (not the keeper key) and — when the platform
216
+ injects `RELAYER_NONCE_URL` + `RELAYER_NONCE_TOKEN` — serializes nonces through
217
+ Convex so concurrent bot invocations sharing one managed key don't collide
218
+ (falling back to RPC pending otherwise). Scaffold with `create_bot --template
219
+ relayer-bot`.
220
+
221
+ ```ts
222
+ import { Relayer, log } from "@livo-build/runtime";
223
+ const relayer = new Relayer(env); // RELAYER_PRIVATE_KEY
224
+ const hash = await relayer.send({ address, abi, functionName, args });
225
+ ```
226
+
227
+ ## Queues
228
+
229
+ Publish to a per-project queue from any deployable that declares `produces: [name]`
230
+ (the platform injects `QUEUE_<NAME>_URL` + `QUEUE_<NAME>_TOKEN`):
231
+
232
+ ```ts
233
+ import { queue } from "@livo-build/runtime"; // or new Queue(env, "settler")
234
+ await queue(env, "settler").send({ orderId });
235
+ await queue(env, "settler").sendBatch([{ a: 1 }, { a: 2 }]);
236
+ ```
237
+
124
238
  ## Local testing
125
239
 
126
240
  Everything runs in Node with a mocked `env` — no platform involved. See
@@ -137,3 +251,7 @@ npm run build # tsc → dist/*.js + .d.ts (what npm consumers install)
137
251
  Legacy pre-1559 txs, multi-chain abstraction beyond a chainId, account
138
252
  abstraction / paymasters, anything non-EVM. Added later only when a second real
139
253
  use case demands it.
254
+
255
+ > Agent-facing copy of this reference lives at the MCP resource
256
+ > **`livo://skill/runtime`**; `test/skill-coverage.test.ts` fails CI if a public
257
+ > export here isn't documented there, so the two can't drift.
package/dist/chain.d.ts CHANGED
@@ -1,15 +1,18 @@
1
- import { type AbiFunction } from "./abi.js";
1
+ import { type AbiFunction, type AbiParameter } from "./abi.js";
2
2
  import { type AbiEvent, type DecodedLog } from "./events.js";
3
3
  import { type Eip1559Tx } from "./tx.js";
4
+ import { type MulticallCall, type MulticallOptions } from "./multicall.js";
4
5
  import { type Hex } from "./hex.js";
5
6
  export declare function isNonceCollision(err: unknown): boolean;
6
7
  export interface ChainOptions {
7
- /** env key holding the JSON-RPC URL. Default "RPC_URL". */
8
+ /** env key holding the JSON-RPC URL(s). One URL, or several comma/whitespace-separated for failover. Default "RPC_URL". */
8
9
  rpcUrlSecret?: string;
9
10
  /** env key holding the 0x signer private key. Omit for read-only usage. Default "KEEPER_PRIVATE_KEY". */
10
11
  signerKeySecret?: string;
11
12
  /** Override the RPC URL directly (takes precedence over env). */
12
13
  rpcUrl?: string;
14
+ /** Override with several RPC URLs for failover (takes precedence over env + rpcUrl). */
15
+ rpcUrls?: string[];
13
16
  /** Override the private key directly (takes precedence over env). */
14
17
  privateKey?: string;
15
18
  /** Override chainId (otherwise read from the RPC on first send). */
@@ -72,11 +75,40 @@ export interface Receipt {
72
75
  contractAddress: Hex | null;
73
76
  raw: Record<string, unknown>;
74
77
  }
78
+ /** A constructor entry has no `name`, so it doesn't fit AbiFunction — accept either. */
79
+ type ConstructorEntry = {
80
+ type: "constructor";
81
+ inputs?: AbiParameter[];
82
+ stateMutability?: string;
83
+ };
84
+ export interface DeployOptions {
85
+ /** Full contract ABI — only the `constructor` entry is read (for arg encoding). */
86
+ abi: ReadonlyArray<AbiFunction | ConstructorEntry>;
87
+ /** Creation bytecode, 0x-prefixed. */
88
+ bytecode: Hex;
89
+ /** Constructor args, ABI-encoded against the constructor's inputs and appended. */
90
+ args?: unknown[];
91
+ value?: bigint;
92
+ /** Override gas (skips estimateGas — recommended for large contracts). */
93
+ gas?: bigint;
94
+ nonce?: bigint;
95
+ maxFeePerGas?: bigint;
96
+ maxPriorityFeePerGas?: bigint;
97
+ /** Receipt wait timeout, ms. Default 120_000. */
98
+ timeoutMs?: number;
99
+ /** Receipt poll interval, ms. Default 2_000. */
100
+ pollMs?: number;
101
+ }
75
102
  interface MinimalEnv {
76
103
  [key: string]: unknown;
77
104
  }
78
105
  export declare class Chain {
106
+ /** Primary RPC URL (first of `rpcUrls`). Kept for back-compat. */
79
107
  readonly rpcUrl: string;
108
+ /** All configured RPC URLs, tried in rotation with failover on transport errors. */
109
+ readonly rpcUrls: string[];
110
+ /** Rotating start offset so load spreads across endpoints. */
111
+ private rpcCursor;
80
112
  private readonly privateKey?;
81
113
  private _chainId?;
82
114
  private readonly opts;
@@ -85,7 +117,13 @@ export declare class Chain {
85
117
  /** chainId passed at construction, if any — lets bindings resolve addresses synchronously. */
86
118
  readonly configuredChainId?: number;
87
119
  constructor(env: MinimalEnv | undefined, options?: ChainOptions);
88
- /** Raw JSON-RPC passthrough. Throws on RPC errors with the node's message. */
120
+ /**
121
+ * Raw JSON-RPC passthrough. Throws on RPC errors with the node's message.
122
+ * With multiple `rpcUrls`, a TRANSPORT failure (network error, 5xx, 429) on one
123
+ * endpoint rotates to the next; a JSON-level `error` (a deterministic chain
124
+ * response, e.g. revert/nonce) is returned as-is — retrying elsewhere wouldn't
125
+ * change it. The start endpoint rotates per call to spread load.
126
+ */
89
127
  rpc<T = unknown>(method: string, params?: unknown[]): Promise<T>;
90
128
  getChainId(): Promise<number>;
91
129
  getBlockNumber(): Promise<bigint>;
@@ -100,6 +138,13 @@ export declare class Chain {
100
138
  readContract(opts: ReadOptions): Promise<unknown>;
101
139
  /** Query + decode event logs. */
102
140
  getLogs(opts: GetLogsOptions): Promise<DecodedLog[]>;
141
+ /**
142
+ * Batch many contract reads into ONE eth_call via Multicall3 (aggregate3).
143
+ * Results are positionally aligned with `calls`; a reverting call yields null
144
+ * when allowFailure (default true) or throws otherwise. All calls observe the
145
+ * same block, so the batch is atomic/consistent.
146
+ */
147
+ multicall(calls: MulticallCall[], opts?: MulticallOptions): Promise<(unknown | null)[]>;
103
148
  /** Low-level: 0x calldata for a function call (no broadcast). */
104
149
  encodeFunction(abi: AbiFunction[], functionName: string, args?: unknown[]): Hex;
105
150
  /** Low-level: sign a fully-specified EIP-1559 tx, returning the raw 0x tx. */
@@ -109,6 +154,17 @@ export declare class Chain {
109
154
  * Returns the transaction hash. Fails loudly on a funding-less signer.
110
155
  */
111
156
  send(opts: SendOptions): Promise<Hex>;
157
+ /**
158
+ * Deploy a contract: build a creation tx (to:null, data = bytecode ++ encoded
159
+ * constructor args), sign + broadcast on the same nonce/fee/gas path as send(),
160
+ * wait for the receipt, and return the deployed address. Note: the default
161
+ * fallbackGas (500k) is low for real contracts — pass `gas` for large ones.
162
+ */
163
+ deploy(opts: DeployOptions): Promise<{
164
+ address: Hex;
165
+ hash: Hex;
166
+ receipt: Receipt;
167
+ }>;
112
168
  /**
113
169
  * Optimistic reliable send (docs/WALLET-SPEC.md "optimistic, assigned at
114
170
  * broadcast"). Resolves the nonce at broadcast (never pre-allocated), retries on a
@@ -132,9 +188,10 @@ export declare class Chain {
132
188
  maxFeePerGas: bigint;
133
189
  maxPriorityFeePerGas: bigint;
134
190
  }>;
135
- /** estimateGas with headroom and a safe fallback when the node reverts the estimate. */
191
+ /** estimateGas with headroom and a safe fallback when the node reverts the estimate.
192
+ * `to: null` estimates a contract-creation tx (the `to` field is omitted). */
136
193
  estimateGas(tx: {
137
- to: Hex;
194
+ to: Hex | null;
138
195
  from: Hex;
139
196
  data: Hex;
140
197
  value: bigint;
package/dist/chain.js CHANGED
@@ -2,10 +2,11 @@
2
2
  // zero hand-rolled crypto or ABI. Constructed from the keeper/server/bot `env`:
3
3
  // reads RPC_URL and (for writes) a signing key, both as env bindings injected at
4
4
  // deploy. Reads need no key; writes derive the signer address from the key.
5
- import { decodeFunctionResult, encodeFunctionData, findFunction, functionFromSignature, } from "./abi.js";
5
+ import { decodeFunctionResult, encodeFunctionData, encodeParameters, findFunction, functionFromSignature, } from "./abi.js";
6
6
  import { decodeLog, encodeFilterTopics, } from "./events.js";
7
7
  import { privateKeyToAddress, signTransaction, transactionHash, } from "./tx.js";
8
- import { hexToBytes } from "./hex.js";
8
+ import { multicall as multicallImpl } from "./multicall.js";
9
+ import { bytesToHex, concatBytes, hexToBytes } from "./hex.js";
9
10
  const GWEI = 1000000000n;
10
11
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
11
12
  // Classify an RPC error as a nonce collision — another sender took the nonce we
@@ -20,8 +21,16 @@ export function isNonceCollision(err) {
20
21
  m.includes("known transaction") ||
21
22
  m.includes("replacement transaction underpriced"));
22
23
  }
24
+ function findConstructor(abi) {
25
+ return abi.find((e) => e.type === "constructor");
26
+ }
23
27
  export class Chain {
28
+ /** Primary RPC URL (first of `rpcUrls`). Kept for back-compat. */
24
29
  rpcUrl;
30
+ /** All configured RPC URLs, tried in rotation with failover on transport errors. */
31
+ rpcUrls;
32
+ /** Rotating start offset so load spreads across endpoints. */
33
+ rpcCursor = 0;
25
34
  privateKey;
26
35
  _chainId;
27
36
  opts;
@@ -32,10 +41,20 @@ export class Chain {
32
41
  constructor(env, options = {}) {
33
42
  const rpcKey = options.rpcUrlSecret ?? "RPC_URL";
34
43
  const keyKey = options.signerKeySecret ?? "KEEPER_PRIVATE_KEY";
35
- this.rpcUrl = options.rpcUrl ?? env?.[rpcKey] ?? "";
36
- if (!this.rpcUrl) {
37
- throw new Error(`Chain: no RPC URL — set the ${rpcKey} secret (set_secret ${rpcKey}=<https rpc>) or pass rpcUrl.`);
44
+ // URL sources, in precedence order: rpcUrls[] rpcUrl → env (which may itself
45
+ // hold several comma/whitespace-separated URLs). Failover rotates across them.
46
+ const fromEnv = env?.[rpcKey] ?? "";
47
+ this.rpcUrls = (options.rpcUrls && options.rpcUrls.length
48
+ ? options.rpcUrls
49
+ : options.rpcUrl
50
+ ? [options.rpcUrl]
51
+ : fromEnv.split(/[\s,]+/))
52
+ .map((u) => u.trim())
53
+ .filter(Boolean);
54
+ if (!this.rpcUrls.length) {
55
+ throw new Error(`Chain: no RPC URL — set the ${rpcKey} secret (set_secret ${rpcKey}=<https rpc>) or pass rpcUrl/rpcUrls.`);
38
56
  }
57
+ this.rpcUrl = this.rpcUrls[0];
39
58
  this.privateKey = options.privateKey ?? env?.[keyKey];
40
59
  this.address = this.privateKey ? privateKeyToAddress(this.privateKey) : null;
41
60
  this._chainId = options.chainId;
@@ -47,19 +66,46 @@ export class Chain {
47
66
  };
48
67
  }
49
68
  // --- raw JSON-RPC ----------------------------------------------------------
50
- /** Raw JSON-RPC passthrough. Throws on RPC errors with the node's message. */
69
+ /**
70
+ * Raw JSON-RPC passthrough. Throws on RPC errors with the node's message.
71
+ * With multiple `rpcUrls`, a TRANSPORT failure (network error, 5xx, 429) on one
72
+ * endpoint rotates to the next; a JSON-level `error` (a deterministic chain
73
+ * response, e.g. revert/nonce) is returned as-is — retrying elsewhere wouldn't
74
+ * change it. The start endpoint rotates per call to spread load.
75
+ */
51
76
  async rpc(method, params = []) {
52
- const res = await fetch(this.rpcUrl, {
53
- method: "POST",
54
- headers: { "content-type": "application/json" },
55
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
56
- });
57
- if (!res.ok)
58
- throw new Error(`RPC ${method} HTTP ${res.status}`);
59
- const json = (await res.json());
60
- if (json.error)
61
- throw new Error(`RPC ${method} error ${json.error.code}: ${json.error.message}`);
62
- return json.result;
77
+ const body = JSON.stringify({ jsonrpc: "2.0", id: 1, method, params });
78
+ const n = this.rpcUrls.length;
79
+ const start = this.rpcCursor++ % n;
80
+ let lastErr;
81
+ for (let i = 0; i < n; i++) {
82
+ const url = this.rpcUrls[(start + i) % n];
83
+ try {
84
+ const res = await fetch(url, {
85
+ method: "POST",
86
+ headers: { "content-type": "application/json" },
87
+ body,
88
+ });
89
+ // Retry-worthy HTTP statuses → try the next endpoint.
90
+ if (res.status >= 500 || res.status === 429) {
91
+ lastErr = new Error(`RPC ${method} HTTP ${res.status}`);
92
+ continue;
93
+ }
94
+ if (!res.ok)
95
+ throw new Error(`RPC ${method} HTTP ${res.status}`);
96
+ const json = (await res.json());
97
+ if (json.error)
98
+ throw new Error(`RPC ${method} error ${json.error.code}: ${json.error.message}`);
99
+ return json.result;
100
+ }
101
+ catch (err) {
102
+ // A 4xx (other than 429) or chain-level error is deterministic — surface it.
103
+ if (err instanceof Error && /HTTP 4|RPC .* error /.test(err.message))
104
+ throw err;
105
+ lastErr = err; // network/transport error → failover to the next endpoint
106
+ }
107
+ }
108
+ throw lastErr ?? new Error(`RPC ${method} failed on all ${n} endpoint(s)`);
63
109
  }
64
110
  async getChainId() {
65
111
  if (this._chainId === undefined) {
@@ -104,6 +150,15 @@ export class Chain {
104
150
  const raw = await this.rpc("eth_getLogs", [filter]);
105
151
  return raw.map((l) => decodeLog(opts.event, l));
106
152
  }
153
+ /**
154
+ * Batch many contract reads into ONE eth_call via Multicall3 (aggregate3).
155
+ * Results are positionally aligned with `calls`; a reverting call yields null
156
+ * when allowFailure (default true) or throws otherwise. All calls observe the
157
+ * same block, so the batch is atomic/consistent.
158
+ */
159
+ multicall(calls, opts) {
160
+ return multicallImpl(this, calls, opts);
161
+ }
107
162
  // --- writes ----------------------------------------------------------------
108
163
  /** Low-level: 0x calldata for a function call (no broadcast). */
109
164
  encodeFunction(abi, functionName, args = []) {
@@ -149,6 +204,54 @@ export class Chain {
149
204
  throw err;
150
205
  }
151
206
  }
207
+ /**
208
+ * Deploy a contract: build a creation tx (to:null, data = bytecode ++ encoded
209
+ * constructor args), sign + broadcast on the same nonce/fee/gas path as send(),
210
+ * wait for the receipt, and return the deployed address. Note: the default
211
+ * fallbackGas (500k) is low for real contracts — pass `gas` for large ones.
212
+ */
213
+ async deploy(opts) {
214
+ if (!this.privateKey || !this.address)
215
+ throw this.noKey();
216
+ const ctor = findConstructor(opts.abi);
217
+ const args = opts.args ?? [];
218
+ if (args.length && !ctor) {
219
+ throw new Error("deploy: constructor args provided but the ABI has no constructor entry.");
220
+ }
221
+ const encodedArgs = args.length ? encodeParameters(ctor?.inputs ?? [], args) : new Uint8Array(0);
222
+ const data = bytesToHex(concatBytes(hexToBytes(opts.bytecode), encodedArgs));
223
+ const value = opts.value ?? 0n;
224
+ const [chainId, nonce, fees] = await Promise.all([
225
+ this.getChainId(),
226
+ opts.nonce !== undefined ? Promise.resolve(opts.nonce) : this.nextNonce(this.address),
227
+ this.feeData(),
228
+ ]);
229
+ const gas = opts.gas ?? (await this.estimateGas({ to: null, from: this.address, data, value }));
230
+ const tx = {
231
+ chainId: BigInt(chainId),
232
+ nonce,
233
+ maxPriorityFeePerGas: opts.maxPriorityFeePerGas ?? fees.maxPriorityFeePerGas,
234
+ maxFeePerGas: opts.maxFeePerGas ?? fees.maxFeePerGas,
235
+ gas,
236
+ to: null,
237
+ value,
238
+ data,
239
+ };
240
+ const raw = signTransaction(tx, this.privateKey);
241
+ let hash;
242
+ try {
243
+ hash = await this.rpc("eth_sendRawTransaction", [raw]);
244
+ }
245
+ catch (err) {
246
+ await this.explainSendFailure(err, value, gas, tx.maxFeePerGas);
247
+ throw err;
248
+ }
249
+ const receipt = await this.waitForReceipt(hash, { timeoutMs: opts.timeoutMs, pollMs: opts.pollMs });
250
+ if (receipt.status === "reverted" || !receipt.contractAddress) {
251
+ throw new Error(`deploy: creation tx ${hash} ${receipt.status === "reverted" ? "reverted" : "produced no contract address"}.`);
252
+ }
253
+ return { address: receipt.contractAddress, hash, receipt };
254
+ }
152
255
  /**
153
256
  * Optimistic reliable send (docs/WALLET-SPEC.md "optimistic, assigned at
154
257
  * broadcast"). Resolves the nonce at broadcast (never pre-allocated), retries on a
@@ -243,12 +346,14 @@ export class Chain {
243
346
  }
244
347
  return { maxFeePerGas: baseFee * 2n + tip, maxPriorityFeePerGas: tip };
245
348
  }
246
- /** estimateGas with headroom and a safe fallback when the node reverts the estimate. */
349
+ /** estimateGas with headroom and a safe fallback when the node reverts the estimate.
350
+ * `to: null` estimates a contract-creation tx (the `to` field is omitted). */
247
351
  async estimateGas(tx) {
248
352
  try {
249
- const est = BigInt(await this.rpc("eth_estimateGas", [
250
- { to: tx.to, from: tx.from, data: tx.data, value: toQuantity(tx.value) },
251
- ]));
353
+ const call = { from: tx.from, data: tx.data, value: toQuantity(tx.value) };
354
+ if (tx.to !== null)
355
+ call.to = tx.to;
356
+ const est = BigInt(await this.rpc("eth_estimateGas", [call]));
252
357
  return (est * this.opts.gasMultiplierPct) / 100n;
253
358
  }
254
359
  catch {
@@ -1,8 +1,15 @@
1
1
  import type { AbiFunction } from "./abi.js";
2
2
  import type { Chain } from "./chain.js";
3
+ import type { MulticallOptions } from "./multicall.js";
3
4
  import type { Hex } from "./hex.js";
4
5
  export type AddressBook = Record<number, Record<string, string>>;
5
6
  export type AbiBook = Record<string, AbiFunction[]>;
7
+ /** A read on this handle to batch through multicall (function + args). */
8
+ export interface HandleMulticallRead {
9
+ functionName: string;
10
+ args?: unknown[];
11
+ allowFailure?: boolean;
12
+ }
6
13
  export interface ContractHandle {
7
14
  readonly address: Hex;
8
15
  readonly abi: AbiFunction[];
@@ -10,6 +17,8 @@ export interface ContractHandle {
10
17
  read: Record<string, (...args: unknown[]) => Promise<unknown>>;
11
18
  /** Write methods (sign + eth_sendRawTransaction) keyed by function name; returns tx hash. */
12
19
  write: Record<string, (...args: unknown[]) => Promise<Hex>>;
20
+ /** Batch several reads on THIS contract into one eth_call (Multicall3). */
21
+ multicallRead(calls: HandleMulticallRead[], opts?: MulticallOptions): Promise<(unknown | null)[]>;
13
22
  }
14
23
  export interface ContractBinding {
15
24
  readonly name: string;
@@ -21,6 +30,16 @@ export interface ContractBinding {
21
30
  address?: Hex;
22
31
  }): ContractHandle;
23
32
  }
33
+ /**
34
+ * Batch reads across DIFFERENT bound contracts into one eth_call. Each entry
35
+ * names a connected handle + a function on it; results are positionally aligned.
36
+ */
37
+ export declare function multicallContracts(chain: Chain, calls: Array<{
38
+ handle: ContractHandle;
39
+ functionName: string;
40
+ args?: unknown[];
41
+ allowFailure?: boolean;
42
+ }>, opts?: MulticallOptions): Promise<(unknown | null)[]>;
24
43
  /**
25
44
  * Build name-addressable contract bindings from generated address/ABI books.
26
45
  * The generated bindings module calls this and exports the result as `contracts`.
package/dist/contracts.js CHANGED
@@ -27,7 +27,21 @@ function buildHandle(chain, address, abi) {
27
27
  read[name] = read[name] ?? ((...args) => chain.readContract({ address, abi, functionName: name, args }));
28
28
  }
29
29
  }
30
- return { address, abi, read, write };
30
+ const multicallRead = (calls, opts) => chain.multicall(calls.map((c) => ({ address, abi, functionName: c.functionName, args: c.args, allowFailure: c.allowFailure })), opts);
31
+ return { address, abi, read, write, multicallRead };
32
+ }
33
+ /**
34
+ * Batch reads across DIFFERENT bound contracts into one eth_call. Each entry
35
+ * names a connected handle + a function on it; results are positionally aligned.
36
+ */
37
+ export function multicallContracts(chain, calls, opts) {
38
+ return chain.multicall(calls.map((c) => ({
39
+ address: c.handle.address,
40
+ abi: c.handle.abi,
41
+ functionName: c.functionName,
42
+ args: c.args,
43
+ allowFailure: c.allowFailure,
44
+ })), opts);
31
45
  }
32
46
  /**
33
47
  * Build name-addressable contract bindings from generated address/ABI books.
package/dist/index.d.ts CHANGED
@@ -1,12 +1,22 @@
1
1
  export { Chain, isNonceCollision } from "./chain.js";
2
- export type { ChainOptions, SendOptions, SendReliableOptions, ReadOptions, GetLogsOptions, Receipt, } from "./chain.js";
2
+ export type { ChainOptions, SendOptions, SendReliableOptions, ReadOptions, GetLogsOptions, Receipt, DeployOptions, } from "./chain.js";
3
+ export { multicall, MULTICALL3_ADDRESS } from "./multicall.js";
4
+ export type { MulticallCall, MulticallOptions } from "./multicall.js";
5
+ export { watchLogs } from "./watch.js";
6
+ export type { WatchLogsOptions, WatchLogsResult } from "./watch.js";
3
7
  export { Relayer } from "./relayer.js";
8
+ export { Queue, queue, queueEnvKey } from "./queue.js";
4
9
  export type { RelayerOptions } from "./relayer.js";
10
+ export { Indexer, indexer, indexerEnvKey, GraphQLError } from "./indexer.js";
11
+ export type { IndexerOptions } from "./indexer.js";
12
+ export { Telegram } from "./telegram.js";
13
+ export type { TelegramOptions, BotMessage, SendMessageOptions } from "./telegram.js";
5
14
  export { Store, STORE_TABLE } from "./store.js";
6
15
  export type { D1Like } from "./store.js";
7
16
  export { log } from "./log.js";
8
17
  export { retry } from "./retry.js";
9
18
  export type { RetryOptions } from "./retry.js";
19
+ export { requireSecret, requireSecrets, getSecret } from "./secret.js";
10
20
  export { encodeFunctionData, encodeParameters, decodeParameters, decodeFunctionResult, functionSelector, functionSignature, functionFromSignature, parseSignature, findFunction, keccak256, } from "./abi.js";
11
21
  export type { AbiFunction, AbiParameter, ParsedSignature } from "./abi.js";
12
22
  export { signTransaction, transactionHashToSign, transactionHash, privateKeyToAddress, toChecksumAddress, } from "./tx.js";
@@ -15,5 +25,5 @@ export { eventTopic, encodeFilterTopics, encodeIndexedTopic, decodeLog, parseEve
15
25
  export type { AbiEvent, RawLog, DecodedLog } from "./events.js";
16
26
  export { bytesToHex, hexToBytes, concatBytes, toBigInt, } from "./hex.js";
17
27
  export type { Hex, Bytes } from "./hex.js";
18
- export { bindContracts } from "./contracts.js";
19
- export type { ContractBinding, ContractHandle, AddressBook, AbiBook } from "./contracts.js";
28
+ export { bindContracts, multicallContracts } from "./contracts.js";
29
+ export type { ContractBinding, ContractHandle, HandleMulticallRead, AddressBook, AbiBook, } from "./contracts.js";
package/dist/index.js CHANGED
@@ -3,17 +3,28 @@
3
3
  // tree-shakeable exports; keep the core small and auditable (it may hold keys).
4
4
  // Layer 1 — Chain: sign + send + read with zero hand-rolled crypto/ABI.
5
5
  export { Chain, isNonceCollision } from "./chain.js";
6
+ // Multicall — batch reads into one eth_call (Multicall3 aggregate3).
7
+ export { multicall, MULTICALL3_ADDRESS } from "./multicall.js";
8
+ // watchLogs — resumable, cursor-persisted event catch-up for keepers.
9
+ export { watchLogs } from "./watch.js";
6
10
  // Relayer — managed signing for bots (custodied RELAYER_PRIVATE_KEY + optional
7
11
  // Convex-serialized nonces). A Chain that defaults to the relayer key.
8
12
  export { Relayer } from "./relayer.js";
13
+ export { Queue, queue, queueEnvKey } from "./queue.js";
14
+ // Indexer — typed GraphQL client for a project's Goldsky subgraph (reads the
15
+ // stable `live`-alias endpoint injected as INDEXER_<NAME>_URL).
16
+ export { Indexer, indexer, indexerEnvKey, GraphQLError } from "./indexer.js";
17
+ // Telegram — webhook verification + reply plumbing for bots.
18
+ export { Telegram } from "./telegram.js";
9
19
  // Layer 3 — State + utilities.
10
20
  export { Store, STORE_TABLE } from "./store.js";
11
21
  export { log } from "./log.js";
12
22
  export { retry } from "./retry.js";
23
+ export { requireSecret, requireSecrets, getSecret } from "./secret.js";
13
24
  // Low-level escape hatches — ABI coding, signing, RLP, hex. Power users only.
14
25
  export { encodeFunctionData, encodeParameters, decodeParameters, decodeFunctionResult, functionSelector, functionSignature, functionFromSignature, parseSignature, findFunction, keccak256, } from "./abi.js";
15
26
  export { signTransaction, transactionHashToSign, transactionHash, privateKeyToAddress, toChecksumAddress, } from "./tx.js";
16
27
  export { eventTopic, encodeFilterTopics, encodeIndexedTopic, decodeLog, parseEventSignature, } from "./events.js";
17
28
  export { bytesToHex, hexToBytes, concatBytes, toBigInt, } from "./hex.js";
18
29
  // Layer 2 — generated contract bindings live in "@livo/runtime/contracts".
19
- export { bindContracts } from "./contracts.js";
30
+ export { bindContracts, multicallContracts } from "./contracts.js";