@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/dist/chain.js ADDED
@@ -0,0 +1,214 @@
1
+ // Chain — the high-leverage layer. Read and write the chain from a Worker with
2
+ // zero hand-rolled crypto or ABI. Constructed from the keeper/server/bot `env`:
3
+ // reads RPC_URL and (for writes) a signing key, both as env bindings injected at
4
+ // deploy. Reads need no key; writes derive the signer address from the key.
5
+ import { decodeFunctionResult, encodeFunctionData, findFunction, functionFromSignature, } from "./abi.js";
6
+ import { decodeLog, encodeFilterTopics, } from "./events.js";
7
+ import { privateKeyToAddress, signTransaction, transactionHash, } from "./tx.js";
8
+ import { hexToBytes } from "./hex.js";
9
+ const GWEI = 1000000000n;
10
+ export class Chain {
11
+ rpcUrl;
12
+ privateKey;
13
+ _chainId;
14
+ opts;
15
+ /** The signer's address (only present when a key is configured). */
16
+ address;
17
+ /** chainId passed at construction, if any — lets bindings resolve addresses synchronously. */
18
+ configuredChainId;
19
+ constructor(env, options = {}) {
20
+ const rpcKey = options.rpcUrlSecret ?? "RPC_URL";
21
+ const keyKey = options.signerKeySecret ?? "KEEPER_PRIVATE_KEY";
22
+ this.rpcUrl = options.rpcUrl ?? env?.[rpcKey] ?? "";
23
+ if (!this.rpcUrl) {
24
+ throw new Error(`Chain: no RPC URL — set the ${rpcKey} secret (set_secret ${rpcKey}=<https rpc>) or pass rpcUrl.`);
25
+ }
26
+ this.privateKey = options.privateKey ?? env?.[keyKey];
27
+ this.address = this.privateKey ? privateKeyToAddress(this.privateKey) : null;
28
+ this._chainId = options.chainId;
29
+ this.configuredChainId = options.chainId;
30
+ this.opts = {
31
+ defaultTipWei: options.defaultTipWei ?? (GWEI * 3n) / 2n, // 1.5 gwei
32
+ gasMultiplierPct: options.gasMultiplierPct ?? 120n,
33
+ fallbackGas: options.fallbackGas ?? 500000n,
34
+ };
35
+ }
36
+ // --- raw JSON-RPC ----------------------------------------------------------
37
+ /** Raw JSON-RPC passthrough. Throws on RPC errors with the node's message. */
38
+ async rpc(method, params = []) {
39
+ const res = await fetch(this.rpcUrl, {
40
+ method: "POST",
41
+ headers: { "content-type": "application/json" },
42
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
43
+ });
44
+ if (!res.ok)
45
+ throw new Error(`RPC ${method} HTTP ${res.status}`);
46
+ const json = (await res.json());
47
+ if (json.error)
48
+ throw new Error(`RPC ${method} error ${json.error.code}: ${json.error.message}`);
49
+ return json.result;
50
+ }
51
+ async getChainId() {
52
+ if (this._chainId === undefined) {
53
+ this._chainId = Number(BigInt(await this.rpc("eth_chainId")));
54
+ }
55
+ return this._chainId;
56
+ }
57
+ async getBlockNumber() {
58
+ return BigInt(await this.rpc("eth_blockNumber"));
59
+ }
60
+ async getBalance(address, block = "latest") {
61
+ return BigInt(await this.rpc("eth_getBalance", [address, block]));
62
+ }
63
+ // --- reads -----------------------------------------------------------------
64
+ /**
65
+ * Convenience read by human-readable signature, e.g.
66
+ * chain.call(addr, "topicCount()(uint256)")
67
+ * chain.call(addr, "balanceOf(address)(uint256)", ["0x..."])
68
+ */
69
+ async call(address, signature, args = []) {
70
+ const fn = functionFromSignature(signature);
71
+ return this.readContract({ address, abi: [fn], functionName: fn.name, args });
72
+ }
73
+ /** Typed contract read against a JSON ABI. Returns the decoded output(s). */
74
+ async readContract(opts) {
75
+ const fn = findFunction(opts.abi, opts.functionName);
76
+ const data = encodeFunctionData(fn, opts.args ?? []);
77
+ const result = await this.rpc("eth_call", [
78
+ { to: opts.address, data },
79
+ opts.block ?? "latest",
80
+ ]);
81
+ return decodeFunctionResult(fn, hexToBytes(result));
82
+ }
83
+ /** Query + decode event logs. */
84
+ async getLogs(opts) {
85
+ const topics = encodeFilterTopics(opts.event, opts.args);
86
+ const filter = { topics };
87
+ if (opts.address)
88
+ filter.address = opts.address;
89
+ filter.fromBlock = blockTag(opts.fromBlock ?? "earliest");
90
+ filter.toBlock = blockTag(opts.toBlock ?? "latest");
91
+ const raw = await this.rpc("eth_getLogs", [filter]);
92
+ return raw.map((l) => decodeLog(opts.event, l));
93
+ }
94
+ // --- writes ----------------------------------------------------------------
95
+ /** Low-level: 0x calldata for a function call (no broadcast). */
96
+ encodeFunction(abi, functionName, args = []) {
97
+ return encodeFunctionData(findFunction(abi, functionName), args);
98
+ }
99
+ /** Low-level: sign a fully-specified EIP-1559 tx, returning the raw 0x tx. */
100
+ signTx(tx) {
101
+ if (!this.privateKey)
102
+ throw this.noKey();
103
+ return signTransaction(tx, this.privateKey);
104
+ }
105
+ /**
106
+ * Encode → fetch nonce/fees → estimate gas (+headroom) → sign → broadcast.
107
+ * Returns the transaction hash. Fails loudly on a funding-less signer.
108
+ */
109
+ async send(opts) {
110
+ if (!this.privateKey || !this.address)
111
+ throw this.noKey();
112
+ const data = this.encodeFunction(opts.abi, opts.functionName, opts.args ?? []);
113
+ const value = opts.value ?? 0n;
114
+ const [chainId, nonce, fees] = await Promise.all([
115
+ this.getChainId(),
116
+ opts.nonce !== undefined ? Promise.resolve(opts.nonce) : this.pendingNonce(this.address),
117
+ this.feeData(),
118
+ ]);
119
+ const gas = opts.gas ?? (await this.estimateGas({ to: opts.address, from: this.address, data, value }));
120
+ const tx = {
121
+ chainId: BigInt(chainId),
122
+ nonce,
123
+ maxPriorityFeePerGas: opts.maxPriorityFeePerGas ?? fees.maxPriorityFeePerGas,
124
+ maxFeePerGas: opts.maxFeePerGas ?? fees.maxFeePerGas,
125
+ gas,
126
+ to: opts.address,
127
+ value,
128
+ data,
129
+ };
130
+ const raw = signTransaction(tx, this.privateKey);
131
+ try {
132
+ return await this.rpc("eth_sendRawTransaction", [raw]);
133
+ }
134
+ catch (err) {
135
+ await this.explainSendFailure(err, value, gas, tx.maxFeePerGas);
136
+ throw err;
137
+ }
138
+ }
139
+ async pendingNonce(address) {
140
+ return BigInt(await this.rpc("eth_getTransactionCount", [address, "pending"]));
141
+ }
142
+ /** Compute maxFee/tip: maxFee = baseFee*2 + tip, tip = configurable default. */
143
+ async feeData() {
144
+ const tip = this.opts.defaultTipWei;
145
+ let baseFee;
146
+ try {
147
+ const block = await this.rpc("eth_getBlockByNumber", ["latest", false]);
148
+ baseFee = block.baseFeePerGas ? BigInt(block.baseFeePerGas) : BigInt(await this.rpc("eth_gasPrice"));
149
+ }
150
+ catch {
151
+ baseFee = BigInt(await this.rpc("eth_gasPrice"));
152
+ }
153
+ return { maxFeePerGas: baseFee * 2n + tip, maxPriorityFeePerGas: tip };
154
+ }
155
+ /** estimateGas with headroom and a safe fallback when the node reverts the estimate. */
156
+ async estimateGas(tx) {
157
+ try {
158
+ const est = BigInt(await this.rpc("eth_estimateGas", [
159
+ { to: tx.to, from: tx.from, data: tx.data, value: toQuantity(tx.value) },
160
+ ]));
161
+ return (est * this.opts.gasMultiplierPct) / 100n;
162
+ }
163
+ catch {
164
+ return this.opts.fallbackGas;
165
+ }
166
+ }
167
+ // --- receipts --------------------------------------------------------------
168
+ async waitForReceipt(hash, opts = {}) {
169
+ const timeout = opts.timeoutMs ?? 120_000;
170
+ const poll = opts.pollMs ?? 2_000;
171
+ const deadline = Date.now() + timeout;
172
+ for (;;) {
173
+ const r = await this.rpc("eth_getTransactionReceipt", [hash]);
174
+ if (r) {
175
+ return {
176
+ transactionHash: r.transactionHash,
177
+ status: BigInt(r.status ?? "0x0") === 1n ? "success" : "reverted",
178
+ blockNumber: BigInt(r.blockNumber ?? "0x0"),
179
+ gasUsed: BigInt(r.gasUsed ?? "0x0"),
180
+ contractAddress: r.contractAddress ?? null,
181
+ raw: r,
182
+ };
183
+ }
184
+ if (Date.now() > deadline) {
185
+ throw new Error(`waitForReceipt: ${hash} not mined within ${timeout}ms`);
186
+ }
187
+ await new Promise((res) => setTimeout(res, poll));
188
+ }
189
+ }
190
+ /** Compute the hash of a raw signed tx (parity with what the node returns). */
191
+ hashRawTx(rawTx) {
192
+ return transactionHash(rawTx);
193
+ }
194
+ // --- internals -------------------------------------------------------------
195
+ noKey() {
196
+ return new Error("Chain: no signer key — set the KEEPER_PRIVATE_KEY secret (or pass signerKeySecret/privateKey). Reads work without one; writes need it.");
197
+ }
198
+ async explainSendFailure(err, value, gas, maxFee) {
199
+ const msg = err instanceof Error ? err.message : String(err);
200
+ if (/insufficient funds|gas \* price|exceeds.*balance/i.test(msg) && this.address) {
201
+ const balance = await this.getBalance(this.address).catch(() => null);
202
+ const needed = value + gas * maxFee;
203
+ throw new Error(`signer wallet ${this.address} can't afford this tx — needs ~${needed} wei` +
204
+ (balance !== null ? `, has ${balance} wei` : "") +
205
+ `. Fund it, or lower gas/value. (RPC: ${msg})`);
206
+ }
207
+ }
208
+ }
209
+ function blockTag(b) {
210
+ return typeof b === "bigint" ? `0x${b.toString(16)}` : b;
211
+ }
212
+ function toQuantity(v) {
213
+ return `0x${v.toString(16)}`;
214
+ }
@@ -0,0 +1,28 @@
1
+ import type { AbiFunction } from "./abi.js";
2
+ import type { Chain } from "./chain.js";
3
+ import type { Hex } from "./hex.js";
4
+ export type AddressBook = Record<number, Record<string, string>>;
5
+ export type AbiBook = Record<string, AbiFunction[]>;
6
+ export interface ContractHandle {
7
+ readonly address: Hex;
8
+ readonly abi: AbiFunction[];
9
+ /** Read methods (eth_call) keyed by function name. */
10
+ read: Record<string, (...args: unknown[]) => Promise<unknown>>;
11
+ /** Write methods (sign + eth_sendRawTransaction) keyed by function name; returns tx hash. */
12
+ write: Record<string, (...args: unknown[]) => Promise<Hex>>;
13
+ }
14
+ export interface ContractBinding {
15
+ readonly name: string;
16
+ /** Resolve the canonical address for a chainId (defaults to the chain's id). */
17
+ addressFor(chainId: number): Hex;
18
+ /** Bind this contract to a Chain, producing callable read/write handles. */
19
+ connect(chain: Chain, opts?: {
20
+ chainId?: number;
21
+ address?: Hex;
22
+ }): ContractHandle;
23
+ }
24
+ /**
25
+ * Build name-addressable contract bindings from generated address/ABI books.
26
+ * The generated bindings module calls this and exports the result as `contracts`.
27
+ */
28
+ export declare function bindContracts(addresses: AddressBook, abis: AbiBook): Record<string, ContractBinding>;
@@ -0,0 +1,77 @@
1
+ // Runtime contract bindings — the consumer side of Layer 2. The platform
2
+ // generates a sibling module (like the frontend's interface/src/livo/contracts.ts)
3
+ // exporting `addresses` (chainId → name → address) and `abis` (name → ABI), both
4
+ // resolved through the SAME canonical pointer the frontend uses. This module turns
5
+ // that generated data into name-addressable, typed-ish contract handles so a keeper
6
+ // never hardcodes an address (the divergence footgun) again.
7
+ //
8
+ // Generated bindings re-export `bindContracts(...)` as `contracts` so callers write:
9
+ // import { contracts } from "@livo/runtime/contracts";
10
+ // const forum = contracts.Forum.connect(chain);
11
+ // await forum.read.topicCount();
12
+ // const hash = await forum.write.createPost(topicId, body);
13
+ function buildHandle(chain, address, abi) {
14
+ const read = {};
15
+ const write = {};
16
+ for (const entry of abi) {
17
+ if ((entry.type ?? "function") !== "function" || !entry.name)
18
+ continue;
19
+ const isView = entry.stateMutability === "view" || entry.stateMutability === "pure";
20
+ const name = entry.name;
21
+ if (isView) {
22
+ read[name] = (...args) => chain.readContract({ address, abi, functionName: name, args });
23
+ }
24
+ else {
25
+ write[name] = (...args) => chain.send({ address, abi, functionName: name, args });
26
+ // Allow reading a non-view fn too (e.g. simulate) — but default to write.
27
+ read[name] = read[name] ?? ((...args) => chain.readContract({ address, abi, functionName: name, args }));
28
+ }
29
+ }
30
+ return { address, abi, read, write };
31
+ }
32
+ /**
33
+ * Build name-addressable contract bindings from generated address/ABI books.
34
+ * The generated bindings module calls this and exports the result as `contracts`.
35
+ */
36
+ export function bindContracts(addresses, abis) {
37
+ const out = {};
38
+ const names = new Set();
39
+ for (const chainId of Object.keys(addresses)) {
40
+ for (const name of Object.keys(addresses[Number(chainId)]))
41
+ names.add(name);
42
+ }
43
+ for (const name of names) {
44
+ const binding = {
45
+ name,
46
+ addressFor(chainId) {
47
+ const addr = addresses[chainId]?.[name];
48
+ if (!addr) {
49
+ const known = Object.keys(addresses).join(", ");
50
+ throw new Error(`No canonical address for ${name} on chain ${chainId} (have chains: ${known || "none"}).`);
51
+ }
52
+ return addr;
53
+ },
54
+ connect(chain, opts) {
55
+ const abi = abis[name];
56
+ if (!abi)
57
+ throw new Error(`No ABI for contract ${name} in generated bindings.`);
58
+ // Resolve via the explicit override, else the configured/looked-up chainId.
59
+ const chainId = opts?.chainId ?? chain.configuredChainId;
60
+ const address = opts?.address ??
61
+ (chainId !== undefined
62
+ ? binding.addressFor(chainId)
63
+ : singleChainAddress(addresses, name));
64
+ return buildHandle(chain, address, abi);
65
+ },
66
+ };
67
+ out[name] = binding;
68
+ }
69
+ return out;
70
+ }
71
+ /** When bindings cover exactly one chain, connect() can resolve without a chainId. */
72
+ function singleChainAddress(addresses, name) {
73
+ const chainIds = Object.keys(addresses).filter((c) => addresses[Number(c)][name]);
74
+ if (chainIds.length === 1)
75
+ return addresses[Number(chainIds[0])][name];
76
+ throw new Error(`${name} is deployed on multiple chains (${chainIds.join(", ")}); pass { chainId } to connect() or set chainId on the Chain.`);
77
+ }
@@ -0,0 +1,37 @@
1
+ import { functionSelector, type AbiParameter } from "./abi.js";
2
+ import { type Hex } from "./hex.js";
3
+ export interface AbiEvent {
4
+ type?: "event";
5
+ name: string;
6
+ inputs: Array<AbiParameter & {
7
+ indexed?: boolean;
8
+ }>;
9
+ anonymous?: boolean;
10
+ }
11
+ export interface RawLog {
12
+ address: Hex;
13
+ topics: Hex[];
14
+ data: Hex;
15
+ blockNumber?: Hex;
16
+ transactionHash?: Hex;
17
+ logIndex?: Hex;
18
+ }
19
+ export interface DecodedLog extends RawLog {
20
+ eventName: string;
21
+ args: Record<string, unknown>;
22
+ }
23
+ /** Parse "Transfer(address indexed from, address indexed to, uint256 value)" form. */
24
+ export declare function parseEventSignature(sig: string): AbiEvent;
25
+ /** keccak256("Name(type,type,...)") — the topic0 for a (non-anonymous) event. */
26
+ export declare function eventTopic(event: AbiEvent | string): Hex;
27
+ /** Encode a single indexed value into its 32-byte topic representation. */
28
+ export declare function encodeIndexedTopic(type: string, value: unknown): Hex;
29
+ /**
30
+ * Build the topics array for an eth_getLogs filter. `args` maps indexed input
31
+ * names (or positions) to values; omit any to leave it unfiltered (null).
32
+ */
33
+ export declare function encodeFilterTopics(event: AbiEvent | string, args?: Record<string, unknown> | unknown[]): (Hex | null)[];
34
+ /** Decode a single raw log into named args using the event's ABI. */
35
+ export declare function decodeLog(event: AbiEvent | string, raw: RawLog): DecodedLog;
36
+ /** Selector-style 4-byte tag isn't used for events, but exported for parity/tests. */
37
+ export { functionSelector };
package/dist/events.js ADDED
@@ -0,0 +1,139 @@
1
+ // Event log helpers — topic0 computation, indexed-arg topic filters, and decoding
2
+ // returned logs back into named args. Covers value-type indexed params (address,
3
+ // uintN/intN, bool, bytesN); dynamic indexed params (string/bytes/arrays) are
4
+ // matched by their keccak hash per the ABI spec but not reversibly decoded.
5
+ import { canonicalType, decodeParameters, functionSelector, keccak256, parseType, } from "./abi.js";
6
+ import { bytesToBigInt, bytesToBigIntSigned, bytesToHex, concatBytes, hexToBytes, toBytes, toBytesSigned, toBigInt, } from "./hex.js";
7
+ /** Parse "Transfer(address indexed from, address indexed to, uint256 value)" form. */
8
+ export function parseEventSignature(sig) {
9
+ const m = /^\s*(?:event\s+)?([a-zA-Z_$][\w$]*)\s*\(([^]*)\)\s*$/.exec(sig);
10
+ if (!m)
11
+ throw new Error(`unparseable event signature: ${sig}`);
12
+ const inputs = splitTop(m[2]).map((part) => {
13
+ const t = part.trim();
14
+ const indexed = /\bindexed\b/.test(t);
15
+ const cleaned = t.replace(/\bindexed\b/, "").trim();
16
+ const tokens = cleaned.split(/\s+/);
17
+ const type = tokens[0];
18
+ const name = tokens[1];
19
+ return { type, name, indexed };
20
+ });
21
+ return { type: "event", name: m[1], inputs };
22
+ }
23
+ function splitTop(s) {
24
+ const out = [];
25
+ let depth = 0;
26
+ let start = 0;
27
+ for (let i = 0; i < s.length; i++) {
28
+ const c = s[i];
29
+ if (c === "(" || c === "[")
30
+ depth++;
31
+ else if (c === ")" || c === "]")
32
+ depth--;
33
+ else if (c === "," && depth === 0) {
34
+ out.push(s.slice(start, i));
35
+ start = i + 1;
36
+ }
37
+ }
38
+ if (s.slice(start).trim())
39
+ out.push(s.slice(start));
40
+ return out;
41
+ }
42
+ function asEvent(event) {
43
+ return typeof event === "string" ? parseEventSignature(event) : event;
44
+ }
45
+ /** keccak256("Name(type,type,...)") — the topic0 for a (non-anonymous) event. */
46
+ export function eventTopic(event) {
47
+ const e = asEvent(event);
48
+ const sig = `${e.name}(${e.inputs.map((i) => canonicalType(parseType(i))).join(",")})`;
49
+ // Reuse the selector machinery (keccak) but keep the full 32-byte hash.
50
+ return bytesToHex(keccak256(new TextEncoder().encode(sig)));
51
+ }
52
+ /** Encode a single indexed value into its 32-byte topic representation. */
53
+ export function encodeIndexedTopic(type, value) {
54
+ const t = parseType(type);
55
+ switch (t.kind) {
56
+ case "uint":
57
+ return bytesToHex(toBytes(toBigInt(value), 32));
58
+ case "int":
59
+ return bytesToHex(toBytesSigned(toBigInt(value), 32));
60
+ case "bool":
61
+ return bytesToHex(toBytes(value ? 1n : 0n, 32));
62
+ case "address": {
63
+ const b = hexToBytes(value);
64
+ return bytesToHex(concatBytes(new Uint8Array(12), b));
65
+ }
66
+ case "bytesN": {
67
+ const b = typeof value === "string" ? hexToBytes(value) : value;
68
+ const out = new Uint8Array(32);
69
+ out.set(b, 0);
70
+ return bytesToHex(out);
71
+ }
72
+ case "bytes":
73
+ case "string": {
74
+ const b = typeof value === "string" && t.kind === "string"
75
+ ? new TextEncoder().encode(value)
76
+ : hexToBytes(value);
77
+ return bytesToHex(keccak256(b)); // dynamic indexed → hashed
78
+ }
79
+ default:
80
+ throw new Error(`cannot index type ${type} as a topic`);
81
+ }
82
+ }
83
+ /**
84
+ * Build the topics array for an eth_getLogs filter. `args` maps indexed input
85
+ * names (or positions) to values; omit any to leave it unfiltered (null).
86
+ */
87
+ export function encodeFilterTopics(event, args) {
88
+ const e = asEvent(event);
89
+ const topics = [eventTopic(e)];
90
+ const indexed = e.inputs.filter((i) => i.indexed);
91
+ indexed.forEach((input, i) => {
92
+ let value = undefined;
93
+ if (Array.isArray(args))
94
+ value = args[i];
95
+ else if (args && input.name && input.name in args)
96
+ value = args[input.name];
97
+ topics.push(value === undefined || value === null ? null : encodeIndexedTopic(input.type, value));
98
+ });
99
+ // Trim trailing nulls so the RPC filter stays minimal.
100
+ while (topics.length > 1 && topics[topics.length - 1] === null)
101
+ topics.pop();
102
+ return topics;
103
+ }
104
+ /** Decode a single raw log into named args using the event's ABI. */
105
+ export function decodeLog(event, raw) {
106
+ const e = asEvent(event);
107
+ const args = {};
108
+ const indexedInputs = e.inputs.filter((i) => i.indexed);
109
+ const nonIndexed = e.inputs.filter((i) => !i.indexed);
110
+ // Indexed args come from topics[1..]; value types are decoded, dynamic stay raw.
111
+ indexedInputs.forEach((input, i) => {
112
+ const topic = raw.topics[i + 1];
113
+ if (topic === undefined)
114
+ return;
115
+ const t = parseType(input.type);
116
+ const bytes = hexToBytes(topic);
117
+ const key = input.name || String(i);
118
+ if (t.kind === "uint")
119
+ args[key] = bytesToBigInt(bytes);
120
+ else if (t.kind === "int")
121
+ args[key] = bytesToBigIntSigned(bytes);
122
+ else if (t.kind === "bool")
123
+ args[key] = bytesToBigInt(bytes) !== 0n;
124
+ else if (t.kind === "address")
125
+ args[key] = bytesToHex(bytes.subarray(12));
126
+ else
127
+ args[key] = topic; // bytesN / dynamic-hashed: surface the raw topic
128
+ });
129
+ // Non-indexed args are ABI-decoded from the data blob.
130
+ if (nonIndexed.length > 0) {
131
+ const decoded = decodeParameters(nonIndexed, hexToBytes(raw.data));
132
+ nonIndexed.forEach((input, i) => {
133
+ args[input.name || `data${i}`] = decoded[i];
134
+ });
135
+ }
136
+ return { ...raw, eventName: e.name, args };
137
+ }
138
+ /** Selector-style 4-byte tag isn't used for events, but exported for parity/tests. */
139
+ export { functionSelector };
package/dist/hex.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export type Hex = `0x${string}`;
2
+ export type Bytes = Uint8Array;
3
+ /** Lowercase 0x-hex for a byte array. */
4
+ export declare function bytesToHex(bytes: Bytes): Hex;
5
+ /** Parse 0x-hex (or bare hex) to bytes. Odd-length input is left-padded a nibble. */
6
+ export declare function hexToBytes(hex: string): Bytes;
7
+ /** Concatenate byte arrays into one. */
8
+ export declare function concatBytes(...parts: Bytes[]): Bytes;
9
+ /** Minimal big-endian byte encoding of a non-negative integer (no leading zeros). */
10
+ export declare function toMinimalBytes(value: bigint): Bytes;
11
+ /** Big-endian fixed-width (default 32) byte encoding of a non-negative integer. */
12
+ export declare function toBytes(value: bigint, width?: number): Bytes;
13
+ /** Two's-complement 32-byte encoding of a possibly-negative integer (int256 family). */
14
+ export declare function toBytesSigned(value: bigint, width?: number): Bytes;
15
+ /** Decode a big-endian byte slice to a non-negative bigint. */
16
+ export declare function bytesToBigInt(bytes: Bytes): bigint;
17
+ /** Interpret a width-byte two's-complement slice as a signed bigint. */
18
+ export declare function bytesToBigIntSigned(bytes: Bytes): bigint;
19
+ /** Coerce common numeric inputs to bigint. */
20
+ export declare function toBigInt(value: bigint | number | string): bigint;
21
+ /** Normalize an 0x-hex string to lowercase with a guaranteed 0x prefix. */
22
+ export declare function asHex(value: string): Hex;
package/dist/hex.js ADDED
@@ -0,0 +1,100 @@
1
+ // Byte / hex primitives shared by the ABI coder, RLP, and the signer. No deps.
2
+ // Everything here is allocation-light and Worker-safe (Web APIs only).
3
+ const HEX_CHARS = "0123456789abcdef";
4
+ /** Lowercase 0x-hex for a byte array. */
5
+ export function bytesToHex(bytes) {
6
+ let out = "0x";
7
+ for (let i = 0; i < bytes.length; i++) {
8
+ out += HEX_CHARS[bytes[i] >> 4] + HEX_CHARS[bytes[i] & 15];
9
+ }
10
+ return out;
11
+ }
12
+ /** Parse 0x-hex (or bare hex) to bytes. Odd-length input is left-padded a nibble. */
13
+ export function hexToBytes(hex) {
14
+ let h = hex.startsWith("0x") ? hex.slice(2) : hex;
15
+ if (h.length % 2)
16
+ h = "0" + h;
17
+ const out = new Uint8Array(h.length / 2);
18
+ for (let i = 0; i < out.length; i++) {
19
+ const byte = Number.parseInt(h.slice(i * 2, i * 2 + 2), 16);
20
+ if (Number.isNaN(byte))
21
+ throw new Error(`invalid hex at byte ${i}: ${hex}`);
22
+ out[i] = byte;
23
+ }
24
+ return out;
25
+ }
26
+ /** Concatenate byte arrays into one. */
27
+ export function concatBytes(...parts) {
28
+ let len = 0;
29
+ for (const p of parts)
30
+ len += p.length;
31
+ const out = new Uint8Array(len);
32
+ let off = 0;
33
+ for (const p of parts) {
34
+ out.set(p, off);
35
+ off += p.length;
36
+ }
37
+ return out;
38
+ }
39
+ /** Minimal big-endian byte encoding of a non-negative integer (no leading zeros). */
40
+ export function toMinimalBytes(value) {
41
+ if (value < 0n)
42
+ throw new Error("toMinimalBytes: negative");
43
+ if (value === 0n)
44
+ return new Uint8Array(0);
45
+ let hex = value.toString(16);
46
+ if (hex.length % 2)
47
+ hex = "0" + hex;
48
+ return hexToBytes(hex);
49
+ }
50
+ /** Big-endian fixed-width (default 32) byte encoding of a non-negative integer. */
51
+ export function toBytes(value, width = 32) {
52
+ if (value < 0n)
53
+ throw new Error("toBytes: use twosComplement for negatives");
54
+ const out = new Uint8Array(width);
55
+ let v = value;
56
+ for (let i = width - 1; i >= 0 && v > 0n; i--) {
57
+ out[i] = Number(v & 0xffn);
58
+ v >>= 8n;
59
+ }
60
+ if (v > 0n)
61
+ throw new Error(`value ${value} does not fit in ${width} bytes`);
62
+ return out;
63
+ }
64
+ /** Two's-complement 32-byte encoding of a possibly-negative integer (int256 family). */
65
+ export function toBytesSigned(value, width = 32) {
66
+ const bits = BigInt(width * 8);
67
+ const mod = 1n << bits;
68
+ const v = ((value % mod) + mod) % mod; // wrap into unsigned range
69
+ return toBytes(v, width);
70
+ }
71
+ /** Decode a big-endian byte slice to a non-negative bigint. */
72
+ export function bytesToBigInt(bytes) {
73
+ let out = 0n;
74
+ for (const b of bytes)
75
+ out = (out << 8n) | BigInt(b);
76
+ return out;
77
+ }
78
+ /** Interpret a width-byte two's-complement slice as a signed bigint. */
79
+ export function bytesToBigIntSigned(bytes) {
80
+ const unsigned = bytesToBigInt(bytes);
81
+ const bits = BigInt(bytes.length * 8);
82
+ const signBit = 1n << (bits - 1n);
83
+ return unsigned >= signBit ? unsigned - (1n << bits) : unsigned;
84
+ }
85
+ /** Coerce common numeric inputs to bigint. */
86
+ export function toBigInt(value) {
87
+ if (typeof value === "bigint")
88
+ return value;
89
+ if (typeof value === "number") {
90
+ if (!Number.isInteger(value))
91
+ throw new Error(`not an integer: ${value}`);
92
+ return BigInt(value);
93
+ }
94
+ return value.startsWith("0x") ? BigInt(value) : BigInt(value);
95
+ }
96
+ /** Normalize an 0x-hex string to lowercase with a guaranteed 0x prefix. */
97
+ export function asHex(value) {
98
+ const h = value.startsWith("0x") ? value : `0x${value}`;
99
+ return h.toLowerCase();
100
+ }
@@ -0,0 +1,17 @@
1
+ export { Chain } from "./chain.js";
2
+ export type { ChainOptions, SendOptions, ReadOptions, GetLogsOptions, Receipt, } from "./chain.js";
3
+ export { Store, STORE_TABLE } from "./store.js";
4
+ export type { D1Like } from "./store.js";
5
+ export { log } from "./log.js";
6
+ export { retry } from "./retry.js";
7
+ export type { RetryOptions } from "./retry.js";
8
+ export { encodeFunctionData, encodeParameters, decodeParameters, decodeFunctionResult, functionSelector, functionSignature, functionFromSignature, parseSignature, findFunction, keccak256, } from "./abi.js";
9
+ export type { AbiFunction, AbiParameter, ParsedSignature } from "./abi.js";
10
+ export { signTransaction, transactionHashToSign, transactionHash, privateKeyToAddress, toChecksumAddress, } from "./tx.js";
11
+ export type { Eip1559Tx } from "./tx.js";
12
+ export { eventTopic, encodeFilterTopics, encodeIndexedTopic, decodeLog, parseEventSignature, } from "./events.js";
13
+ export type { AbiEvent, RawLog, DecodedLog } from "./events.js";
14
+ export { bytesToHex, hexToBytes, concatBytes, toBigInt, } from "./hex.js";
15
+ export type { Hex, Bytes } from "./hex.js";
16
+ export { bindContracts } from "./contracts.js";
17
+ export type { ContractBinding, ContractHandle, AddressBook, AbiBook } from "./contracts.js";
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ // @livo/runtime — the standard library available to every Livo compute target
2
+ // that isn't a contract or a static frontend (keepers, servers, bots). Named,
3
+ // tree-shakeable exports; keep the core small and auditable (it may hold keys).
4
+ // Layer 1 — Chain: sign + send + read with zero hand-rolled crypto/ABI.
5
+ export { Chain } from "./chain.js";
6
+ // Layer 3 — State + utilities.
7
+ export { Store, STORE_TABLE } from "./store.js";
8
+ export { log } from "./log.js";
9
+ export { retry } from "./retry.js";
10
+ // Low-level escape hatches — ABI coding, signing, RLP, hex. Power users only.
11
+ export { encodeFunctionData, encodeParameters, decodeParameters, decodeFunctionResult, functionSelector, functionSignature, functionFromSignature, parseSignature, findFunction, keccak256, } from "./abi.js";
12
+ export { signTransaction, transactionHashToSign, transactionHash, privateKeyToAddress, toChecksumAddress, } from "./tx.js";
13
+ export { eventTopic, encodeFilterTopics, encodeIndexedTopic, decodeLog, parseEventSignature, } from "./events.js";
14
+ export { bytesToHex, hexToBytes, concatBytes, toBigInt, } from "./hex.js";
15
+ // Layer 2 — generated contract bindings live in "@livo/runtime/contracts".
16
+ export { bindContracts } from "./contracts.js";
package/dist/log.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ type Fields = Record<string, unknown>;
2
+ export declare const log: {
3
+ info: (msg: string, fields?: Fields) => void;
4
+ warn: (msg: string, fields?: Fields) => void;
5
+ error: (msg: string, err?: Fields | Error) => void;
6
+ debug: (msg: string, fields?: Fields) => void;
7
+ };
8
+ export {};