@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 +139 -0
- package/dist/abi.d.ts +58 -0
- package/dist/abi.js +373 -0
- package/dist/chain.d.ts +118 -0
- package/dist/chain.js +214 -0
- package/dist/contracts.d.ts +28 -0
- package/dist/contracts.js +77 -0
- package/dist/events.d.ts +37 -0
- package/dist/events.js +139 -0
- package/dist/hex.d.ts +22 -0
- package/dist/hex.js +100 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +16 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.js +27 -0
- package/dist/retry.d.ts +11 -0
- package/dist/retry.js +23 -0
- package/dist/rlp.d.ts +4 -0
- package/dist/rlp.js +20 -0
- package/dist/store.d.ts +31 -0
- package/dist/store.js +60 -0
- package/dist/tx.d.ts +25 -0
- package/dist/tx.js +86 -0
- package/package.json +47 -0
package/dist/log.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Structured, consistently-prefixed logging. Plain console under the hood so the
|
|
2
|
+
// keeper run-reporter (which captures console.*) still records everything.
|
|
3
|
+
function emit(level, msg, fields) {
|
|
4
|
+
const prefix = `[livo:${level}]`;
|
|
5
|
+
if (fields instanceof Error) {
|
|
6
|
+
console[level](prefix, msg, fields.stack || fields.message);
|
|
7
|
+
}
|
|
8
|
+
else if (fields && Object.keys(fields).length > 0) {
|
|
9
|
+
let rendered;
|
|
10
|
+
try {
|
|
11
|
+
rendered = JSON.stringify(fields);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
rendered = String(fields);
|
|
15
|
+
}
|
|
16
|
+
console[level](prefix, msg, rendered);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console[level](prefix, msg);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const log = {
|
|
23
|
+
info: (msg, fields) => emit("info", msg, fields),
|
|
24
|
+
warn: (msg, fields) => emit("warn", msg, fields),
|
|
25
|
+
error: (msg, err) => emit("error", msg, err),
|
|
26
|
+
debug: (msg, fields) => emit("debug", msg, fields),
|
|
27
|
+
};
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
/** Total attempts (including the first). Default 3. */
|
|
3
|
+
tries?: number;
|
|
4
|
+
/** Base delay before the first retry, doubled each round. Default 500ms. */
|
|
5
|
+
backoffMs?: number;
|
|
6
|
+
/** Cap on any single backoff. Default 10s. */
|
|
7
|
+
maxBackoffMs?: number;
|
|
8
|
+
/** Return false to stop retrying a given error. Default: always retry. */
|
|
9
|
+
shouldRetry?: (err: unknown, attempt: number) => boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function retry<T>(fn: () => Promise<T>, opts?: RetryOptions): Promise<T>;
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Retry with exponential backoff — for flaky public RPC endpoints. Worker-safe
|
|
2
|
+
// (uses setTimeout via a Promise; no Node timers).
|
|
3
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4
|
+
export async function retry(fn, opts = {}) {
|
|
5
|
+
const tries = opts.tries ?? 3;
|
|
6
|
+
const base = opts.backoffMs ?? 500;
|
|
7
|
+
const cap = opts.maxBackoffMs ?? 10_000;
|
|
8
|
+
let lastErr;
|
|
9
|
+
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
10
|
+
try {
|
|
11
|
+
return await fn();
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
lastErr = err;
|
|
15
|
+
if (attempt === tries)
|
|
16
|
+
break;
|
|
17
|
+
if (opts.shouldRetry && !opts.shouldRetry(err, attempt))
|
|
18
|
+
break;
|
|
19
|
+
await sleep(Math.min(cap, base * 2 ** (attempt - 1)));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
throw lastErr;
|
|
23
|
+
}
|
package/dist/rlp.d.ts
ADDED
package/dist/rlp.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Minimal RLP encoder — just enough for EIP-1559 (type-2) transaction encoding.
|
|
2
|
+
// Items are byte arrays or nested lists of items. We never decode here.
|
|
3
|
+
import { concatBytes, toMinimalBytes } from "./hex.js";
|
|
4
|
+
function encodeLength(len, offset) {
|
|
5
|
+
if (len < 56)
|
|
6
|
+
return new Uint8Array([offset + len]);
|
|
7
|
+
const lenBytes = toMinimalBytes(BigInt(len));
|
|
8
|
+
return concatBytes(new Uint8Array([offset + 55 + lenBytes.length]), lenBytes);
|
|
9
|
+
}
|
|
10
|
+
/** RLP-encode a byte string or (possibly nested) list. */
|
|
11
|
+
export function rlpEncode(item) {
|
|
12
|
+
if (item instanceof Uint8Array) {
|
|
13
|
+
// Single byte in [0x00, 0x7f] is its own encoding.
|
|
14
|
+
if (item.length === 1 && item[0] < 0x80)
|
|
15
|
+
return item;
|
|
16
|
+
return concatBytes(encodeLength(item.length, 0x80), item);
|
|
17
|
+
}
|
|
18
|
+
const payload = concatBytes(...item.map(rlpEncode));
|
|
19
|
+
return concatBytes(encodeLength(payload.length, 0xc0), payload);
|
|
20
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface D1Like {
|
|
2
|
+
prepare(query: string): {
|
|
3
|
+
bind(...values: unknown[]): {
|
|
4
|
+
first<T = unknown>(colName?: string): Promise<T | null>;
|
|
5
|
+
run(): Promise<unknown>;
|
|
6
|
+
all<T = unknown>(): Promise<{
|
|
7
|
+
results: T[];
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
exec?(query: string): Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
export declare const STORE_TABLE = "livo_kv";
|
|
14
|
+
export declare class Store {
|
|
15
|
+
private readonly db;
|
|
16
|
+
private readonly table;
|
|
17
|
+
private ready;
|
|
18
|
+
constructor(db: D1Like, table?: string);
|
|
19
|
+
/** Lazily create the backing table once per instance. */
|
|
20
|
+
private init;
|
|
21
|
+
/** Get a raw string value, or null if absent. */
|
|
22
|
+
get(key: string): Promise<string | null>;
|
|
23
|
+
/** Upsert a raw string value. */
|
|
24
|
+
set(key: string, value: string): Promise<void>;
|
|
25
|
+
/** Delete a key (no-op if absent). */
|
|
26
|
+
delete(key: string): Promise<void>;
|
|
27
|
+
/** Get a JSON-parsed value, or null if absent. */
|
|
28
|
+
getJSON<T>(key: string): Promise<T | null>;
|
|
29
|
+
/** Upsert a JSON-serialized value. */
|
|
30
|
+
setJSON<T>(key: string, value: T): Promise<void>;
|
|
31
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Typed key/value persistence over the project's D1 database (bound as env.DB).
|
|
2
|
+
// Replaces the hand-rolled `CREATE TABLE IF NOT EXISTS kv (k,v)` every stateful
|
|
3
|
+
// keeper writes. The table name/shape is stable and documented: callers can rely
|
|
4
|
+
// on `livo_kv(k TEXT PRIMARY KEY, v TEXT, updated_at INTEGER)`.
|
|
5
|
+
export const STORE_TABLE = "livo_kv";
|
|
6
|
+
export class Store {
|
|
7
|
+
db;
|
|
8
|
+
table;
|
|
9
|
+
ready = null;
|
|
10
|
+
constructor(db, table = STORE_TABLE) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.table = table;
|
|
13
|
+
if (!db)
|
|
14
|
+
throw new Error("Store: no D1 binding — pass env.DB (is the keeper's D1 bound?)");
|
|
15
|
+
}
|
|
16
|
+
/** Lazily create the backing table once per instance. */
|
|
17
|
+
init() {
|
|
18
|
+
if (!this.ready) {
|
|
19
|
+
const ddl = `CREATE TABLE IF NOT EXISTS ${this.table} (k TEXT PRIMARY KEY, v TEXT, updated_at INTEGER)`;
|
|
20
|
+
this.ready = this.db
|
|
21
|
+
.prepare(ddl)
|
|
22
|
+
.bind()
|
|
23
|
+
.run()
|
|
24
|
+
.then(() => undefined);
|
|
25
|
+
}
|
|
26
|
+
return this.ready;
|
|
27
|
+
}
|
|
28
|
+
/** Get a raw string value, or null if absent. */
|
|
29
|
+
async get(key) {
|
|
30
|
+
await this.init();
|
|
31
|
+
const row = await this.db
|
|
32
|
+
.prepare(`SELECT v FROM ${this.table} WHERE k = ?`)
|
|
33
|
+
.bind(key)
|
|
34
|
+
.first();
|
|
35
|
+
return row ? row.v : null;
|
|
36
|
+
}
|
|
37
|
+
/** Upsert a raw string value. */
|
|
38
|
+
async set(key, value) {
|
|
39
|
+
await this.init();
|
|
40
|
+
await this.db
|
|
41
|
+
.prepare(`INSERT INTO ${this.table} (k, v, updated_at) VALUES (?, ?, ?)
|
|
42
|
+
ON CONFLICT(k) DO UPDATE SET v = excluded.v, updated_at = excluded.updated_at`)
|
|
43
|
+
.bind(key, value, Date.now())
|
|
44
|
+
.run();
|
|
45
|
+
}
|
|
46
|
+
/** Delete a key (no-op if absent). */
|
|
47
|
+
async delete(key) {
|
|
48
|
+
await this.init();
|
|
49
|
+
await this.db.prepare(`DELETE FROM ${this.table} WHERE k = ?`).bind(key).run();
|
|
50
|
+
}
|
|
51
|
+
/** Get a JSON-parsed value, or null if absent. */
|
|
52
|
+
async getJSON(key) {
|
|
53
|
+
const raw = await this.get(key);
|
|
54
|
+
return raw === null ? null : JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
/** Upsert a JSON-serialized value. */
|
|
57
|
+
async setJSON(key, value) {
|
|
58
|
+
await this.set(key, JSON.stringify(value));
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/tx.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Bytes, type Hex } from "./hex.js";
|
|
2
|
+
export interface Eip1559Tx {
|
|
3
|
+
chainId: bigint;
|
|
4
|
+
nonce: bigint;
|
|
5
|
+
maxPriorityFeePerGas: bigint;
|
|
6
|
+
maxFeePerGas: bigint;
|
|
7
|
+
gas: bigint;
|
|
8
|
+
to: Hex | null;
|
|
9
|
+
value: bigint;
|
|
10
|
+
data: Hex;
|
|
11
|
+
accessList?: Array<{
|
|
12
|
+
address: Hex;
|
|
13
|
+
storageKeys: Hex[];
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
/** The hash a signer signs for an EIP-1559 tx. */
|
|
17
|
+
export declare function transactionHashToSign(tx: Eip1559Tx): Bytes;
|
|
18
|
+
/** Sign an EIP-1559 tx and return the raw 0x-serialized transaction ready to broadcast. */
|
|
19
|
+
export declare function signTransaction(tx: Eip1559Tx, privateKey: string): Hex;
|
|
20
|
+
/** The keccak256 transaction hash of a raw signed tx (what eth_sendRawTransaction returns). */
|
|
21
|
+
export declare function transactionHash(rawTx: Hex): Hex;
|
|
22
|
+
/** EIP-55 checksummed address derived from a private key. */
|
|
23
|
+
export declare function privateKeyToAddress(privateKey: string): Hex;
|
|
24
|
+
/** Apply EIP-55 mixed-case checksum to a 20-byte 0x address. */
|
|
25
|
+
export declare function toChecksumAddress(address: string): Hex;
|
package/dist/tx.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// EIP-1559 (type-2) transaction encoding + signing. Mirrors viem's
|
|
2
|
+
// signTransaction byte-for-byte (verified in the test suite). secp256k1 signing
|
|
3
|
+
// uses noble with low-S enforced (the canonical form Ethereum requires).
|
|
4
|
+
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
5
|
+
import { keccak_256 } from "@noble/hashes/sha3";
|
|
6
|
+
import { rlpEncode } from "./rlp.js";
|
|
7
|
+
import { bytesToHex, concatBytes, hexToBytes, toMinimalBytes, } from "./hex.js";
|
|
8
|
+
/** RLP item for a quantity: minimal big-endian bytes (0 → empty string). */
|
|
9
|
+
function q(value) {
|
|
10
|
+
return toMinimalBytes(value);
|
|
11
|
+
}
|
|
12
|
+
function addressItem(to) {
|
|
13
|
+
return to ? hexToBytes(to) : new Uint8Array(0);
|
|
14
|
+
}
|
|
15
|
+
function accessListItems(tx) {
|
|
16
|
+
return (tx.accessList ?? []).map((entry) => [
|
|
17
|
+
hexToBytes(entry.address),
|
|
18
|
+
entry.storageKeys.map((k) => hexToBytes(k)),
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
/** The 9-field unsigned payload, prefixed with the 0x02 type byte. */
|
|
22
|
+
function unsignedPayload(tx) {
|
|
23
|
+
const fields = [
|
|
24
|
+
q(tx.chainId),
|
|
25
|
+
q(tx.nonce),
|
|
26
|
+
q(tx.maxPriorityFeePerGas),
|
|
27
|
+
q(tx.maxFeePerGas),
|
|
28
|
+
q(tx.gas),
|
|
29
|
+
addressItem(tx.to),
|
|
30
|
+
q(tx.value),
|
|
31
|
+
hexToBytes(tx.data),
|
|
32
|
+
accessListItems(tx),
|
|
33
|
+
];
|
|
34
|
+
return concatBytes(new Uint8Array([0x02]), rlpEncode(fields));
|
|
35
|
+
}
|
|
36
|
+
/** The hash a signer signs for an EIP-1559 tx. */
|
|
37
|
+
export function transactionHashToSign(tx) {
|
|
38
|
+
return keccak_256(unsignedPayload(tx));
|
|
39
|
+
}
|
|
40
|
+
/** Normalize a 0x private key to 32 raw bytes. */
|
|
41
|
+
function normalizeKey(privateKey) {
|
|
42
|
+
const k = privateKey.trim().replace(/^0x/, "");
|
|
43
|
+
if (!/^[0-9a-fA-F]{64}$/.test(k))
|
|
44
|
+
throw new Error("private key must be 32 bytes (64 hex chars)");
|
|
45
|
+
return hexToBytes(k);
|
|
46
|
+
}
|
|
47
|
+
/** Sign an EIP-1559 tx and return the raw 0x-serialized transaction ready to broadcast. */
|
|
48
|
+
export function signTransaction(tx, privateKey) {
|
|
49
|
+
const hash = transactionHashToSign(tx);
|
|
50
|
+
const sig = secp256k1.sign(hash, normalizeKey(privateKey), { lowS: true });
|
|
51
|
+
const signed = [
|
|
52
|
+
q(tx.chainId),
|
|
53
|
+
q(tx.nonce),
|
|
54
|
+
q(tx.maxPriorityFeePerGas),
|
|
55
|
+
q(tx.maxFeePerGas),
|
|
56
|
+
q(tx.gas),
|
|
57
|
+
addressItem(tx.to),
|
|
58
|
+
q(tx.value),
|
|
59
|
+
hexToBytes(tx.data),
|
|
60
|
+
accessListItems(tx),
|
|
61
|
+
q(BigInt(sig.recovery)), // yParity
|
|
62
|
+
q(sig.r),
|
|
63
|
+
q(sig.s),
|
|
64
|
+
];
|
|
65
|
+
return bytesToHex(concatBytes(new Uint8Array([0x02]), rlpEncode(signed)));
|
|
66
|
+
}
|
|
67
|
+
/** The keccak256 transaction hash of a raw signed tx (what eth_sendRawTransaction returns). */
|
|
68
|
+
export function transactionHash(rawTx) {
|
|
69
|
+
return bytesToHex(keccak_256(hexToBytes(rawTx)));
|
|
70
|
+
}
|
|
71
|
+
/** EIP-55 checksummed address derived from a private key. */
|
|
72
|
+
export function privateKeyToAddress(privateKey) {
|
|
73
|
+
const pub = secp256k1.getPublicKey(normalizeKey(privateKey), false).slice(1); // drop 0x04
|
|
74
|
+
const addr = keccak_256(pub).slice(-20);
|
|
75
|
+
return toChecksumAddress(bytesToHex(addr));
|
|
76
|
+
}
|
|
77
|
+
/** Apply EIP-55 mixed-case checksum to a 20-byte 0x address. */
|
|
78
|
+
export function toChecksumAddress(address) {
|
|
79
|
+
const lower = address.toLowerCase().replace(/^0x/, "");
|
|
80
|
+
const hash = bytesToHex(keccak_256(new TextEncoder().encode(lower))).slice(2);
|
|
81
|
+
let out = "0x";
|
|
82
|
+
for (let i = 0; i < lower.length; i++) {
|
|
83
|
+
out += parseInt(hash[i], 16) >= 8 ? lower[i].toUpperCase() : lower[i];
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@livo-build/runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Livo runtime — chain signing/reads, D1 state, and logging for keepers, servers, and bots.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./contracts": {
|
|
15
|
+
"types": "./dist/contracts.d.ts",
|
|
16
|
+
"import": "./dist/contracts.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": ["dist"],
|
|
20
|
+
"engines": { "node": ">=18" },
|
|
21
|
+
"keywords": ["livo", "web3", "keeper", "eip1559", "evm"],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/livo-projects/livo-mcp.git",
|
|
28
|
+
"directory": "packages/runtime"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
32
|
+
"build": "tsc -p tsconfig.build.json",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"prepublishOnly": "npm run build",
|
|
35
|
+
"clean": "rm -rf dist"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@noble/curves": "^1.9.7",
|
|
39
|
+
"@noble/hashes": "^1.8.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.10.0",
|
|
43
|
+
"typescript": "^5.7.0",
|
|
44
|
+
"vitest": "^2.1.0",
|
|
45
|
+
"viem": "^2.52.2"
|
|
46
|
+
}
|
|
47
|
+
}
|