@livo-build/runtime 0.1.0 → 0.2.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.d.ts CHANGED
@@ -94,6 +94,13 @@ export declare class Chain {
94
94
  */
95
95
  send(opts: SendOptions): Promise<Hex>;
96
96
  pendingNonce(address: Hex): Promise<bigint>;
97
+ /**
98
+ * The nonce to use for the next send (when the caller didn't pin one). Defaults
99
+ * to the RPC pending count — fine for a single signer. Subclasses (see Relayer)
100
+ * override this to serialize nonces across concurrent invocations that share one
101
+ * managed signing key, which the per-Worker RPC count alone can't coordinate.
102
+ */
103
+ protected nextNonce(address: Hex): Promise<bigint>;
97
104
  /** Compute maxFee/tip: maxFee = baseFee*2 + tip, tip = configurable default. */
98
105
  feeData(): Promise<{
99
106
  maxFeePerGas: bigint;
package/dist/chain.js CHANGED
@@ -113,7 +113,7 @@ export class Chain {
113
113
  const value = opts.value ?? 0n;
114
114
  const [chainId, nonce, fees] = await Promise.all([
115
115
  this.getChainId(),
116
- opts.nonce !== undefined ? Promise.resolve(opts.nonce) : this.pendingNonce(this.address),
116
+ opts.nonce !== undefined ? Promise.resolve(opts.nonce) : this.nextNonce(this.address),
117
117
  this.feeData(),
118
118
  ]);
119
119
  const gas = opts.gas ?? (await this.estimateGas({ to: opts.address, from: this.address, data, value }));
@@ -139,6 +139,15 @@ export class Chain {
139
139
  async pendingNonce(address) {
140
140
  return BigInt(await this.rpc("eth_getTransactionCount", [address, "pending"]));
141
141
  }
142
+ /**
143
+ * The nonce to use for the next send (when the caller didn't pin one). Defaults
144
+ * to the RPC pending count — fine for a single signer. Subclasses (see Relayer)
145
+ * override this to serialize nonces across concurrent invocations that share one
146
+ * managed signing key, which the per-Worker RPC count alone can't coordinate.
147
+ */
148
+ nextNonce(address) {
149
+ return this.pendingNonce(address);
150
+ }
142
151
  /** Compute maxFee/tip: maxFee = baseFee*2 + tip, tip = configurable default. */
143
152
  async feeData() {
144
153
  const tip = this.opts.defaultTipWei;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { Chain } from "./chain.js";
2
2
  export type { ChainOptions, SendOptions, ReadOptions, GetLogsOptions, Receipt, } from "./chain.js";
3
+ export { Relayer } from "./relayer.js";
4
+ export type { RelayerOptions } from "./relayer.js";
3
5
  export { Store, STORE_TABLE } from "./store.js";
4
6
  export type { D1Like } from "./store.js";
5
7
  export { log } from "./log.js";
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@
3
3
  // tree-shakeable exports; keep the core small and auditable (it may hold keys).
4
4
  // Layer 1 — Chain: sign + send + read with zero hand-rolled crypto/ABI.
5
5
  export { Chain } from "./chain.js";
6
+ // Relayer — managed signing for bots (custodied RELAYER_PRIVATE_KEY + optional
7
+ // Convex-serialized nonces). A Chain that defaults to the relayer key.
8
+ export { Relayer } from "./relayer.js";
6
9
  // Layer 3 — State + utilities.
7
10
  export { Store, STORE_TABLE } from "./store.js";
8
11
  export { log } from "./log.js";
@@ -0,0 +1,20 @@
1
+ import { Chain, type ChainOptions } from "./chain.js";
2
+ import type { Hex } from "./hex.js";
3
+ export interface RelayerOptions extends ChainOptions {
4
+ /** Convex /relayer/nonce endpoint. Default: env RELAYER_NONCE_URL. */
5
+ nonceUrl?: string;
6
+ /** Bearer scoping the nonce allocation to this bot. Default: env RELAYER_NONCE_TOKEN. */
7
+ nonceToken?: string;
8
+ }
9
+ interface MinimalEnv {
10
+ [key: string]: unknown;
11
+ }
12
+ export declare class Relayer extends Chain {
13
+ private readonly nonceUrl?;
14
+ private readonly nonceToken?;
15
+ constructor(env: MinimalEnv | undefined, options?: RelayerOptions);
16
+ /** True when this relayer will serialize nonces through Convex (vs. RPC pending). */
17
+ get serializesNonce(): boolean;
18
+ protected nextNonce(address: Hex): Promise<bigint>;
19
+ }
20
+ export {};
@@ -0,0 +1,54 @@
1
+ // Relayer — the managed signing layer for bots. A bot's relayer wallet is a
2
+ // platform-custodied key (a SEPARATE key from the project's keeper, so a bot and
3
+ // a keeper never collide on the same address's nonce), injected at deploy as the
4
+ // RELAYER_PRIVATE_KEY binding. So a bot just does `new Relayer(env).send(...)` —
5
+ // no key handling, no faucet ritual, no hand-rolled crypto.
6
+ //
7
+ // When the platform also injects RELAYER_NONCE_URL + RELAYER_NONCE_TOKEN, the
8
+ // relayer asks Convex for a *serialized* nonce before each send. Convex mutations
9
+ // are serializable, so two concurrent bot invocations sharing one relayer key get
10
+ // strictly increasing nonces instead of both reading the same RPC pending count
11
+ // and colliding. With the bindings absent (e.g. local dev), it transparently
12
+ // falls back to the RPC pending count — same behavior as a plain Chain.
13
+ import { Chain } from "./chain.js";
14
+ export class Relayer extends Chain {
15
+ nonceUrl;
16
+ nonceToken;
17
+ constructor(env, options = {}) {
18
+ const { nonceUrl, nonceToken, ...chainOpts } = options;
19
+ // The relayer key is the signer by default — NOT the keeper key.
20
+ super(env, { signerKeySecret: chainOpts.signerKeySecret ?? "RELAYER_PRIVATE_KEY", ...chainOpts });
21
+ this.nonceUrl = nonceUrl ?? env?.RELAYER_NONCE_URL;
22
+ this.nonceToken = nonceToken ?? env?.RELAYER_NONCE_TOKEN;
23
+ }
24
+ /** True when this relayer will serialize nonces through Convex (vs. RPC pending). */
25
+ get serializesNonce() {
26
+ return Boolean(this.nonceUrl && this.nonceToken);
27
+ }
28
+ // Ask Convex for the next serialized nonce, passing the RPC pending count so the
29
+ // server-side counter self-heals when the wallet advanced out-of-band. Any
30
+ // failure (no bindings, network, non-200) falls back to the RPC pending count —
31
+ // the relayer never blocks on the coordinator being reachable.
32
+ async nextNonce(address) {
33
+ const observedPending = await this.pendingNonce(address);
34
+ if (!this.nonceUrl || !this.nonceToken)
35
+ return observedPending;
36
+ try {
37
+ const chainId = await this.getChainId();
38
+ const res = await fetch(this.nonceUrl, {
39
+ method: "POST",
40
+ headers: { "content-type": "application/json", authorization: `Bearer ${this.nonceToken}` },
41
+ body: JSON.stringify({ address, chainId, observedPending: Number(observedPending) }),
42
+ });
43
+ if (res.ok) {
44
+ const j = (await res.json());
45
+ if (j.ok && typeof j.nonce === "number" && j.nonce >= 0)
46
+ return BigInt(j.nonce);
47
+ }
48
+ }
49
+ catch {
50
+ /* fall through to the RPC pending count */
51
+ }
52
+ return observedPending;
53
+ }
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livo-build/runtime",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Livo runtime — chain signing/reads, D1 state, and logging for keepers, servers, and bots.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",