@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/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 };
|
package/dist/chain.d.ts
ADDED
|
@@ -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 {};
|