@livo-build/runtime 0.1.0

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 ADDED
@@ -0,0 +1,139 @@
1
+ # @livo-build/runtime
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).
6
+
7
+ 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.
11
+
12
+ ```ts
13
+ import { Chain, Store, log } from "@livo-build/runtime";
14
+
15
+ export default {
16
+ async scheduled(event, env, ctx) {
17
+ const chain = new Chain(env); // reads RPC_URL + KEEPER_PRIVATE_KEY from env
18
+ const count = await chain.call(FORUM, "topicCount()(uint256)");
19
+ const hash = await chain.send({ address: FORUM, abi: FORUM_ABI, functionName: "createPost", args: [count, "gm"] });
20
+ const receipt = await chain.waitForReceipt(hash);
21
+ log.info("posted", { hash, status: receipt.status });
22
+ },
23
+ };
24
+ ```
25
+
26
+ ## Delivery model — explicit import, registry-installed, bundled at deploy
27
+
28
+ You write **normal imported code** that is locally runnable and type-checked. The
29
+ Livo deploy step produces the self-contained single-module Worker the platform
30
+ requires. There are **no ambient globals** — what you read in the source is what
31
+ runs.
32
+
33
+ `@livo-build/runtime` is published to npm (like `@livo-build/livo`). A
34
+ keeper/bot/server that imports it ships a `package.json` declaring the dependency:
35
+
36
+ - On deploy, the presence of a `package.json` with deps routes the target through
37
+ the platform's existing **build container** (`npm install` + `esbuild`), which
38
+ resolves `@livo-build/runtime` from the registry and bundles it — plus its
39
+ audited `@noble` crypto — into one self-contained Worker module. No hand-bundling.
40
+ - Targets with **no `package.json`** deploy exactly as before — the build step is
41
+ 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
43
+ update can't silently change a working keeper.
44
+
45
+ Scaffold the common path with `create_keeper --template runtime-keeper`.
46
+
47
+ ## Layer 1 — `Chain`
48
+
49
+ Reads need no key; writes need a signer key on `env`.
50
+
51
+ ```ts
52
+ const chain = new Chain(env, {
53
+ rpcUrlSecret: "RPC_URL", // default
54
+ signerKeySecret: "KEEPER_PRIVATE_KEY", // omit for read-only
55
+ });
56
+
57
+ // reads
58
+ await chain.call(address, "balanceOf(address)(uint256)", [who]);
59
+ await chain.readContract({ address, abi, functionName, args });
60
+ await chain.getLogs({ address, event: "Transfer(address indexed from, address indexed to, uint256 value)", args: { from }, fromBlock });
61
+ await chain.getBalance(addr);
62
+ chain.address; // signer address (null when read-only)
63
+
64
+ // writes — encode → nonce(pending) → fees → estimateGas(+20%) → sign EIP-1559 → broadcast
65
+ const hash = await chain.send({ address, abi, functionName, args });
66
+ const receipt = await chain.waitForReceipt(hash, { timeoutMs, pollMs });
67
+
68
+ // escape hatches
69
+ chain.encodeFunction(abi, fn, args); // 0x calldata
70
+ chain.signTx(txFields); // raw 0x EIP-1559 tx
71
+ chain.rpc(method, params); // raw JSON-RPC
72
+ ```
73
+
74
+ Defaults (configurable): nonce `pending`; `tip = 1.5 gwei`; `maxFee = baseFee*2 +
75
+ tip`; `estimateGas * 1.2` with a `500_000` fallback when the node reverts the
76
+ estimate. A funding-less signer fails loudly: *"signer wallet 0x… can't afford
77
+ this tx — needs ~N wei, has M wei."*
78
+
79
+ Encoding and signing are validated **byte-for-byte against viem** in CI
80
+ (`test/abi.test.ts`, `test/tx.test.ts`). EIP-1559 (type-2) only for v1.
81
+
82
+ ## Layer 2 — Contract bindings
83
+
84
+ `sync_contract_bindings` emits a pure-data bindings module —
85
+ `keepers/_livo/contracts.js` — from the **same canonical pointer (`pickCanonical`)
86
+ the frontend's `interface/src/livo/contracts.ts` uses**, so runtime and UI can
87
+ never watch different deployments. Feed it to `bindContracts` for
88
+ name-addressable, typed contract handles:
89
+
90
+ ```ts
91
+ import { Chain, bindContracts } from "@livo-build/runtime";
92
+ import { addresses, abis } from "../_livo/contracts.js"; // generated, canonical
93
+
94
+ const contracts = bindContracts(addresses, abis);
95
+ const forum = contracts.Forum.connect(chain); // address resolved from the canonical registry
96
+ await forum.read.topicCount();
97
+ const hash = await forum.write.createPost(topicId, body);
98
+ ```
99
+
100
+ (The module is pure data and dependency-free so it resolves in the deploy build
101
+ container without the keeper's `node_modules`. `@livo-build/runtime/contracts`
102
+ exports `bindContracts` and the binding types.)
103
+
104
+ ## Layer 3 — State + utilities
105
+
106
+ ```ts
107
+ const store = new Store(env.DB); // typed KV over D1; auto-creates `livo_kv`
108
+ await store.get("heartbeat"); // string | null
109
+ await store.set("heartbeat", "3");
110
+ await store.getJSON<Config>("config");
111
+ await store.setJSON("config", obj);
112
+
113
+ log.info("posted", { tx, topicId }); // structured, consistently prefixed
114
+ log.error("send failed", err);
115
+
116
+ import { retry } from "@livo-build/runtime";
117
+ await retry(() => chain.send(...), { tries: 3, backoffMs: 500 }); // flaky RPCs
118
+ ```
119
+
120
+ `Store` replaces the hand-rolled `CREATE TABLE IF NOT EXISTS kv (k,v)`. The table
121
+ is `livo_kv(k TEXT PRIMARY KEY, v TEXT, updated_at INTEGER)` — stable and
122
+ documented.
123
+
124
+ ## Local testing
125
+
126
+ Everything runs in Node with a mocked `env` — no platform involved. See
127
+ `test/keeper-harness.test.ts` for the template: a fake JSON-RPC handler, an
128
+ in-memory D1, and a real keeper handler driven end-to-end.
129
+
130
+ ```sh
131
+ npm test # vitest: viem-equivalence + keeper harness
132
+ npm run build # tsc → dist/*.js + .d.ts (what npm consumers install)
133
+ ```
134
+
135
+ ## Out of scope (v1)
136
+
137
+ Legacy pre-1559 txs, multi-chain abstraction beyond a chainId, account
138
+ abstraction / paymasters, anything non-EVM. Added later only when a second real
139
+ use case demands it.
package/dist/abi.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { type Bytes, type Hex } from "./hex.js";
2
+ export interface AbiParameter {
3
+ type: string;
4
+ name?: string;
5
+ components?: AbiParameter[];
6
+ }
7
+ export interface AbiFunction {
8
+ type?: "function";
9
+ name: string;
10
+ inputs?: AbiParameter[];
11
+ outputs?: AbiParameter[];
12
+ stateMutability?: string;
13
+ }
14
+ type ParsedType = {
15
+ kind: "uint" | "int";
16
+ bits: number;
17
+ } | {
18
+ kind: "address" | "bool" | "bytes" | "string";
19
+ } | {
20
+ kind: "bytesN";
21
+ size: number;
22
+ } | {
23
+ kind: "array";
24
+ element: ParsedType;
25
+ length: number | null;
26
+ } | {
27
+ kind: "tuple";
28
+ components: ParsedType[];
29
+ names?: string[];
30
+ };
31
+ declare function parseType(input: string | AbiParameter): ParsedType;
32
+ declare function isDynamic(t: ParsedType): boolean;
33
+ /** Encode a parameter list to ABI bytes. Accepts type strings or JSON-ABI params. */
34
+ export declare function encodeParameters(params: (string | AbiParameter)[], values: unknown[]): Bytes;
35
+ declare function canonicalType(t: ParsedType): string;
36
+ /** keccak256 of UTF-8 input as bytes. */
37
+ export declare function keccak256(data: Bytes): Bytes;
38
+ /** The 4-byte selector for a canonical signature like "transfer(address,uint256)". */
39
+ export declare function functionSelector(signature: string): Bytes;
40
+ /** Build the canonical "name(type,type)" signature from a parsed function. */
41
+ export declare function functionSignature(fn: AbiFunction): string;
42
+ /** Resolve a function from a JSON ABI by name (first match). */
43
+ export declare function findFunction(abi: AbiFunction[], name: string): AbiFunction;
44
+ /** 0x calldata = selector ++ encoded args, from a JSON-ABI function. */
45
+ export declare function encodeFunctionData(fn: AbiFunction, args?: unknown[]): Hex;
46
+ /** Decode an ABI-encoded return blob against a parameter list. */
47
+ export declare function decodeParameters(params: (string | AbiParameter)[], data: Bytes): unknown[];
48
+ /** Decode a function's outputs from a call result. Returns the single value when there's one output. */
49
+ export declare function decodeFunctionResult(fn: AbiFunction, data: Bytes): unknown;
50
+ export interface ParsedSignature {
51
+ name: string;
52
+ inputs: string[];
53
+ outputs: string[];
54
+ }
55
+ export declare function parseSignature(sig: string): ParsedSignature;
56
+ /** Build a JSON-ABI function from a human-readable signature. */
57
+ export declare function functionFromSignature(sig: string): AbiFunction;
58
+ export { parseType, canonicalType, isDynamic };
package/dist/abi.js ADDED
@@ -0,0 +1,373 @@
1
+ // Solidity ABI coder — encoding + decoding for the type set keepers actually use:
2
+ // uintN / intN, address, bool, bytesN, bytes, string, fixed/dynamic arrays, and
3
+ // tuples (structs), nested arbitrarily. Validated byte-for-byte against viem in
4
+ // the test suite. The encoder is the hand-rolled-ABI footgun this runtime removes.
5
+ import { keccak_256 } from "@noble/hashes/sha3";
6
+ import { bytesToBigInt, bytesToBigIntSigned, bytesToHex, concatBytes, hexToBytes, toBytes, toBytesSigned, toBigInt, } from "./hex.js";
7
+ const TUPLE_RE = /^\((.*)\)((?:\[\d*\])*)$/;
8
+ const ARRAY_RE = /^(.*)\[(\d*)\]$/;
9
+ /** Split a comma-separated tuple body, respecting nested parentheses/brackets. */
10
+ function splitTopLevel(s) {
11
+ const out = [];
12
+ let depth = 0;
13
+ let start = 0;
14
+ for (let i = 0; i < s.length; i++) {
15
+ const c = s[i];
16
+ if (c === "(" || c === "[")
17
+ depth++;
18
+ else if (c === ")" || c === "]")
19
+ depth--;
20
+ else if (c === "," && depth === 0) {
21
+ out.push(s.slice(start, i));
22
+ start = i + 1;
23
+ }
24
+ }
25
+ const last = s.slice(start);
26
+ if (last.trim() !== "" || out.length > 0)
27
+ out.push(last);
28
+ return out.map((x) => x.trim()).filter((x) => x.length > 0);
29
+ }
30
+ function parseType(input) {
31
+ // JSON-ABI tuple carries its shape in `components`.
32
+ if (typeof input !== "string") {
33
+ if (input.type.startsWith("tuple")) {
34
+ const inner = {
35
+ kind: "tuple",
36
+ components: (input.components ?? []).map(parseType),
37
+ names: (input.components ?? []).map((c) => c.name ?? ""),
38
+ };
39
+ const suffix = input.type.slice("tuple".length);
40
+ return suffix ? wrapArrays(inner, suffix) : inner;
41
+ }
42
+ return parseType(input.type);
43
+ }
44
+ const str = input.trim();
45
+ const arr = ARRAY_RE.exec(str);
46
+ if (arr) {
47
+ return {
48
+ kind: "array",
49
+ element: parseType(arr[1]),
50
+ length: arr[2] === "" ? null : Number(arr[2]),
51
+ };
52
+ }
53
+ const tup = TUPLE_RE.exec(str);
54
+ if (tup) {
55
+ const components = splitTopLevel(tup[1]).map(parseType);
56
+ const base = { kind: "tuple", components };
57
+ return tup[2] ? wrapArrays(base, tup[2]) : base;
58
+ }
59
+ if (str === "address" || str === "bool" || str === "bytes" || str === "string") {
60
+ return { kind: str };
61
+ }
62
+ if (str === "uint")
63
+ return { kind: "uint", bits: 256 };
64
+ if (str === "int")
65
+ return { kind: "int", bits: 256 };
66
+ let m = /^uint(\d+)$/.exec(str);
67
+ if (m)
68
+ return { kind: "uint", bits: Number(m[1]) };
69
+ m = /^int(\d+)$/.exec(str);
70
+ if (m)
71
+ return { kind: "int", bits: Number(m[1]) };
72
+ m = /^bytes(\d+)$/.exec(str);
73
+ if (m)
74
+ return { kind: "bytesN", size: Number(m[1]) };
75
+ throw new Error(`unsupported ABI type: ${str}`);
76
+ }
77
+ /** Apply trailing `[..]` array suffixes (outermost last) to a base type. */
78
+ function wrapArrays(base, suffix) {
79
+ const dims = [];
80
+ const re = /\[(\d*)\]/g;
81
+ let m;
82
+ while ((m = re.exec(suffix)))
83
+ dims.push(m[1] === "" ? null : Number(m[1]));
84
+ // First-found dim is the innermost in Solidity type syntax (T[a][b] = (T[a])[b]).
85
+ let t = base;
86
+ for (const length of dims)
87
+ t = { kind: "array", element: t, length };
88
+ return t;
89
+ }
90
+ function isDynamic(t) {
91
+ switch (t.kind) {
92
+ case "bytes":
93
+ case "string":
94
+ return true;
95
+ case "array":
96
+ return t.length === null || isDynamic(t.element);
97
+ case "tuple":
98
+ return t.components.some(isDynamic);
99
+ default:
100
+ return false;
101
+ }
102
+ }
103
+ function encodeValue(t, value) {
104
+ switch (t.kind) {
105
+ case "uint":
106
+ return { dynamic: false, bytes: toBytes(toBigInt(value), 32) };
107
+ case "int":
108
+ return { dynamic: false, bytes: toBytesSigned(toBigInt(value), 32) };
109
+ case "bool":
110
+ return { dynamic: false, bytes: toBytes(value ? 1n : 0n, 32) };
111
+ case "address": {
112
+ const b = hexToBytes(value);
113
+ if (b.length !== 20)
114
+ throw new Error(`address must be 20 bytes: ${value}`);
115
+ return { dynamic: false, bytes: concatBytes(new Uint8Array(12), b) };
116
+ }
117
+ case "bytesN": {
118
+ const b = typeof value === "string" ? hexToBytes(value) : value;
119
+ if (b.length !== t.size)
120
+ throw new Error(`bytes${t.size} expects ${t.size} bytes, got ${b.length}`);
121
+ const out = new Uint8Array(32);
122
+ out.set(b, 0); // right-padded
123
+ return { dynamic: false, bytes: out };
124
+ }
125
+ case "bytes": {
126
+ const b = typeof value === "string" ? hexToBytes(value) : value;
127
+ return { dynamic: true, bytes: encodeBytesLike(b) };
128
+ }
129
+ case "string": {
130
+ const b = new TextEncoder().encode(value);
131
+ return { dynamic: true, bytes: encodeBytesLike(b) };
132
+ }
133
+ case "array": {
134
+ const items = value;
135
+ if (t.length !== null && items.length !== t.length)
136
+ throw new Error(`expected ${t.length} elements, got ${items.length}`);
137
+ const body = encodeTuple(items.map(() => t.element), items);
138
+ if (t.length === null) {
139
+ // dynamic array: length prefix + packed body
140
+ return { dynamic: true, bytes: concatBytes(toBytes(BigInt(items.length), 32), body) };
141
+ }
142
+ return { dynamic: isDynamic(t.element), bytes: body };
143
+ }
144
+ case "tuple": {
145
+ const vals = tupleValuesInOrder(t, value);
146
+ const body = encodeTuple(t.components, vals);
147
+ return { dynamic: isDynamic(t), bytes: body };
148
+ }
149
+ }
150
+ }
151
+ /** Accept a tuple value as an array (positional) or an object keyed by component name. */
152
+ function tupleValuesInOrder(t, value) {
153
+ if (Array.isArray(value))
154
+ return value;
155
+ if (value && typeof value === "object") {
156
+ const names = t.names;
157
+ if (!names || names.some((n) => !n)) {
158
+ throw new Error("tuple passed as object but its ABI components are unnamed — pass an array");
159
+ }
160
+ return names.map((n) => value[n]);
161
+ }
162
+ throw new Error(`tuple value must be an array or object, got ${typeof value}`);
163
+ }
164
+ function encodeBytesLike(b) {
165
+ const padded = new Uint8Array(Math.ceil(b.length / 32) * 32);
166
+ padded.set(b, 0);
167
+ return concatBytes(toBytes(BigInt(b.length), 32), padded);
168
+ }
169
+ /** Head/tail encode a fixed list of typed values (the core ABI algorithm). */
170
+ function encodeTuple(types, values) {
171
+ if (types.length !== values.length)
172
+ throw new Error(`arity mismatch: ${types.length} types vs ${values.length} values`);
173
+ const parts = types.map((t, i) => encodeValue(t, values[i]));
174
+ let headLen = 0;
175
+ for (const p of parts)
176
+ headLen += p.dynamic ? 32 : p.bytes.length;
177
+ const heads = [];
178
+ const tails = [];
179
+ let tailOffset = headLen;
180
+ for (const p of parts) {
181
+ if (p.dynamic) {
182
+ heads.push(toBytes(BigInt(tailOffset), 32));
183
+ tails.push(p.bytes);
184
+ tailOffset += p.bytes.length;
185
+ }
186
+ else {
187
+ heads.push(p.bytes);
188
+ }
189
+ }
190
+ return concatBytes(...heads, ...tails);
191
+ }
192
+ /** Encode a parameter list to ABI bytes. Accepts type strings or JSON-ABI params. */
193
+ export function encodeParameters(params, values) {
194
+ return encodeTuple(params.map(parseType), values);
195
+ }
196
+ // --- function selectors / calldata -------------------------------------------
197
+ function canonicalType(t) {
198
+ switch (t.kind) {
199
+ case "uint":
200
+ return `uint${t.bits}`;
201
+ case "int":
202
+ return `int${t.bits}`;
203
+ case "bytesN":
204
+ return `bytes${t.size}`;
205
+ case "address":
206
+ case "bool":
207
+ case "bytes":
208
+ case "string":
209
+ return t.kind;
210
+ case "array":
211
+ return `${canonicalType(t.element)}[${t.length ?? ""}]`;
212
+ case "tuple":
213
+ return `(${t.components.map(canonicalType).join(",")})`;
214
+ }
215
+ }
216
+ /** keccak256 of UTF-8 input as bytes. */
217
+ export function keccak256(data) {
218
+ return keccak_256(data);
219
+ }
220
+ /** The 4-byte selector for a canonical signature like "transfer(address,uint256)". */
221
+ export function functionSelector(signature) {
222
+ return keccak_256(new TextEncoder().encode(signature)).slice(0, 4);
223
+ }
224
+ /** Build the canonical "name(type,type)" signature from a parsed function. */
225
+ export function functionSignature(fn) {
226
+ const inputs = (fn.inputs ?? []).map((p) => canonicalType(parseType(p)));
227
+ return `${fn.name}(${inputs.join(",")})`;
228
+ }
229
+ /** Resolve a function from a JSON ABI by name (first match). */
230
+ export function findFunction(abi, name) {
231
+ const fn = abi.find((e) => (e.type === "function" || e.type === undefined) && e.name === name);
232
+ if (!fn)
233
+ throw new Error(`function "${name}" not found in ABI`);
234
+ return fn;
235
+ }
236
+ /** 0x calldata = selector ++ encoded args, from a JSON-ABI function. */
237
+ export function encodeFunctionData(fn, args = []) {
238
+ const selector = functionSelector(functionSignature(fn));
239
+ const encoded = encodeParameters(fn.inputs ?? [], args);
240
+ return bytesToHex(concatBytes(selector, encoded));
241
+ }
242
+ function decodeValue(t, state, baseOffset, headOffset) {
243
+ const slot = state.data.subarray(headOffset, headOffset + 32);
244
+ switch (t.kind) {
245
+ case "uint":
246
+ return { value: bytesToBigInt(slot), nextHead: headOffset + 32 };
247
+ case "int":
248
+ return { value: bytesToBigIntSigned(slot), nextHead: headOffset + 32 };
249
+ case "bool":
250
+ return { value: bytesToBigInt(slot) !== 0n, nextHead: headOffset + 32 };
251
+ case "address":
252
+ return { value: bytesToHex(slot.subarray(12)), nextHead: headOffset + 32 };
253
+ case "bytesN":
254
+ return { value: bytesToHex(slot.subarray(0, t.size)), nextHead: headOffset + 32 };
255
+ case "bytes":
256
+ case "string": {
257
+ const ptr = baseOffset + Number(bytesToBigInt(slot));
258
+ const len = Number(bytesToBigInt(state.data.subarray(ptr, ptr + 32)));
259
+ const raw = state.data.subarray(ptr + 32, ptr + 32 + len);
260
+ const value = t.kind === "string" ? new TextDecoder().decode(raw) : bytesToHex(raw);
261
+ return { value, nextHead: headOffset + 32 };
262
+ }
263
+ case "array": {
264
+ if (t.length === null) {
265
+ const ptr = baseOffset + Number(bytesToBigInt(slot));
266
+ const len = Number(bytesToBigInt(state.data.subarray(ptr, ptr + 32)));
267
+ const elemBase = ptr + 32;
268
+ const value = decodeFixedSequence(Array(len).fill(t.element), state, elemBase);
269
+ return { value, nextHead: headOffset + 32 };
270
+ }
271
+ if (isDynamic(t.element)) {
272
+ const ptr = baseOffset + Number(bytesToBigInt(slot));
273
+ const value = decodeFixedSequence(Array(t.length).fill(t.element), state, ptr);
274
+ return { value, nextHead: headOffset + 32 };
275
+ }
276
+ // static fixed array: inline, consumes length*32(ish) bytes of head
277
+ const types = Array(t.length).fill(t.element);
278
+ const { values, consumed } = decodeInlineSequence(types, state, baseOffset, headOffset);
279
+ return { value: values, nextHead: headOffset + consumed };
280
+ }
281
+ case "tuple": {
282
+ if (isDynamic(t)) {
283
+ const ptr = baseOffset + Number(bytesToBigInt(slot));
284
+ const value = decodeFixedSequence(t.components, state, ptr);
285
+ return { value: tupleToObject(t, value), nextHead: headOffset + 32 };
286
+ }
287
+ const { values, consumed } = decodeInlineSequence(t.components, state, baseOffset, headOffset);
288
+ return { value: tupleToObject(t, values), nextHead: headOffset + consumed };
289
+ }
290
+ }
291
+ }
292
+ function tupleToObject(t, values) {
293
+ if (!t.names || t.names.every((n) => !n))
294
+ return values;
295
+ const obj = {};
296
+ t.components.forEach((_, i) => {
297
+ if (t.names[i])
298
+ obj[t.names[i]] = values[i];
299
+ });
300
+ return obj;
301
+ }
302
+ /** Decode a sequence laid out with its own head starting at `base` (offsets relative to base). */
303
+ function decodeFixedSequence(types, state, base) {
304
+ const out = [];
305
+ let head = base;
306
+ for (const t of types) {
307
+ const { value, nextHead } = decodeValue(t, state, base, head);
308
+ out.push(value);
309
+ head = nextHead;
310
+ }
311
+ return out;
312
+ }
313
+ /** Decode static types inline within an enclosing head (no new base). */
314
+ function decodeInlineSequence(types, state, base, start) {
315
+ const out = [];
316
+ let head = start;
317
+ for (const t of types) {
318
+ const { value, nextHead } = decodeValue(t, state, base, head);
319
+ out.push(value);
320
+ head = nextHead;
321
+ }
322
+ return { values: out, consumed: head - start };
323
+ }
324
+ /** Decode an ABI-encoded return blob against a parameter list. */
325
+ export function decodeParameters(params, data) {
326
+ const types = params.map(parseType);
327
+ return decodeFixedSequence(types, { data }, 0);
328
+ }
329
+ /** Decode a function's outputs from a call result. Returns the single value when there's one output. */
330
+ export function decodeFunctionResult(fn, data) {
331
+ const outputs = fn.outputs ?? [];
332
+ const decoded = decodeParameters(outputs, data);
333
+ return outputs.length === 1 ? decoded[0] : decoded;
334
+ }
335
+ export function parseSignature(sig) {
336
+ const m = /^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(([^]*)$/.exec(sig);
337
+ if (!m)
338
+ throw new Error(`unparseable signature: ${sig}`);
339
+ const name = m[1];
340
+ // Re-find the two top-level paren groups from the original string.
341
+ const rest = sig.slice(sig.indexOf("(")).trim();
342
+ const groups = [];
343
+ let depth = 0;
344
+ let start = -1;
345
+ for (let i = 0; i < rest.length; i++) {
346
+ if (rest[i] === "(") {
347
+ if (depth === 0)
348
+ start = i + 1;
349
+ depth++;
350
+ }
351
+ else if (rest[i] === ")") {
352
+ depth--;
353
+ if (depth === 0)
354
+ groups.push(rest.slice(start, i));
355
+ }
356
+ }
357
+ return {
358
+ name,
359
+ inputs: groups[0] !== undefined ? splitTopLevel(groups[0]) : [],
360
+ outputs: groups[1] !== undefined ? splitTopLevel(groups[1]) : [],
361
+ };
362
+ }
363
+ /** Build a JSON-ABI function from a human-readable signature. */
364
+ export function functionFromSignature(sig) {
365
+ const p = parseSignature(sig);
366
+ return {
367
+ type: "function",
368
+ name: p.name,
369
+ inputs: p.inputs.map((type) => ({ type })),
370
+ outputs: p.outputs.map((type) => ({ type })),
371
+ };
372
+ }
373
+ export { parseType, canonicalType, isDynamic };
@@ -0,0 +1,118 @@
1
+ import { type AbiFunction } from "./abi.js";
2
+ import { type AbiEvent, type DecodedLog } from "./events.js";
3
+ import { type Eip1559Tx } from "./tx.js";
4
+ import { type Hex } from "./hex.js";
5
+ export interface ChainOptions {
6
+ /** env key holding the JSON-RPC URL. Default "RPC_URL". */
7
+ rpcUrlSecret?: string;
8
+ /** env key holding the 0x signer private key. Omit for read-only usage. Default "KEEPER_PRIVATE_KEY". */
9
+ signerKeySecret?: string;
10
+ /** Override the RPC URL directly (takes precedence over env). */
11
+ rpcUrl?: string;
12
+ /** Override the private key directly (takes precedence over env). */
13
+ privateKey?: string;
14
+ /** Override chainId (otherwise read from the RPC on first send). */
15
+ chainId?: number;
16
+ /** Default tip in wei (maxPriorityFeePerGas). Default 1.5 gwei. */
17
+ defaultTipWei?: bigint;
18
+ /** Gas estimate multiplier in percent. Default 120 (20% headroom). */
19
+ gasMultiplierPct?: bigint;
20
+ /** Fallback gas limit when estimateGas reverts. Default 500_000. */
21
+ fallbackGas?: bigint;
22
+ }
23
+ export interface SendOptions {
24
+ address: Hex;
25
+ abi: AbiFunction[];
26
+ functionName: string;
27
+ args?: unknown[];
28
+ value?: bigint;
29
+ /** Override nonce (default: pending count). */
30
+ nonce?: bigint;
31
+ /** Override gas limit (skips estimateGas). */
32
+ gas?: bigint;
33
+ maxFeePerGas?: bigint;
34
+ maxPriorityFeePerGas?: bigint;
35
+ }
36
+ export interface ReadOptions {
37
+ address: Hex;
38
+ abi: AbiFunction[];
39
+ functionName: string;
40
+ args?: unknown[];
41
+ /** Block tag for the call. Default "latest". */
42
+ block?: Hex | "latest" | "pending";
43
+ }
44
+ export interface GetLogsOptions {
45
+ address?: Hex | Hex[];
46
+ event: AbiEvent | string;
47
+ args?: Record<string, unknown> | unknown[];
48
+ fromBlock?: bigint | "latest" | "earliest";
49
+ toBlock?: bigint | "latest" | "earliest";
50
+ }
51
+ export interface Receipt {
52
+ transactionHash: Hex;
53
+ status: "success" | "reverted";
54
+ blockNumber: bigint;
55
+ gasUsed: bigint;
56
+ contractAddress: Hex | null;
57
+ raw: Record<string, unknown>;
58
+ }
59
+ interface MinimalEnv {
60
+ [key: string]: unknown;
61
+ }
62
+ export declare class Chain {
63
+ readonly rpcUrl: string;
64
+ private readonly privateKey?;
65
+ private _chainId?;
66
+ private readonly opts;
67
+ /** The signer's address (only present when a key is configured). */
68
+ readonly address: Hex | null;
69
+ /** chainId passed at construction, if any — lets bindings resolve addresses synchronously. */
70
+ readonly configuredChainId?: number;
71
+ constructor(env: MinimalEnv | undefined, options?: ChainOptions);
72
+ /** Raw JSON-RPC passthrough. Throws on RPC errors with the node's message. */
73
+ rpc<T = unknown>(method: string, params?: unknown[]): Promise<T>;
74
+ getChainId(): Promise<number>;
75
+ getBlockNumber(): Promise<bigint>;
76
+ getBalance(address: Hex, block?: Hex | "latest" | "pending"): Promise<bigint>;
77
+ /**
78
+ * Convenience read by human-readable signature, e.g.
79
+ * chain.call(addr, "topicCount()(uint256)")
80
+ * chain.call(addr, "balanceOf(address)(uint256)", ["0x..."])
81
+ */
82
+ call(address: Hex, signature: string, args?: unknown[]): Promise<unknown>;
83
+ /** Typed contract read against a JSON ABI. Returns the decoded output(s). */
84
+ readContract(opts: ReadOptions): Promise<unknown>;
85
+ /** Query + decode event logs. */
86
+ getLogs(opts: GetLogsOptions): Promise<DecodedLog[]>;
87
+ /** Low-level: 0x calldata for a function call (no broadcast). */
88
+ encodeFunction(abi: AbiFunction[], functionName: string, args?: unknown[]): Hex;
89
+ /** Low-level: sign a fully-specified EIP-1559 tx, returning the raw 0x tx. */
90
+ signTx(tx: Eip1559Tx): Hex;
91
+ /**
92
+ * Encode → fetch nonce/fees → estimate gas (+headroom) → sign → broadcast.
93
+ * Returns the transaction hash. Fails loudly on a funding-less signer.
94
+ */
95
+ send(opts: SendOptions): Promise<Hex>;
96
+ pendingNonce(address: Hex): Promise<bigint>;
97
+ /** Compute maxFee/tip: maxFee = baseFee*2 + tip, tip = configurable default. */
98
+ feeData(): Promise<{
99
+ maxFeePerGas: bigint;
100
+ maxPriorityFeePerGas: bigint;
101
+ }>;
102
+ /** estimateGas with headroom and a safe fallback when the node reverts the estimate. */
103
+ estimateGas(tx: {
104
+ to: Hex;
105
+ from: Hex;
106
+ data: Hex;
107
+ value: bigint;
108
+ }): Promise<bigint>;
109
+ waitForReceipt(hash: Hex, opts?: {
110
+ timeoutMs?: number;
111
+ pollMs?: number;
112
+ }): Promise<Receipt>;
113
+ /** Compute the hash of a raw signed tx (parity with what the node returns). */
114
+ hashRawTx(rawTx: Hex): Hex;
115
+ private noKey;
116
+ private explainSendFailure;
117
+ }
118
+ export {};