@livo-build/runtime 0.2.2 → 0.2.3

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 CHANGED
@@ -12,7 +12,8 @@ project's subgraph, the Telegram webhook dance, talking to D1, and basic
12
12
  logging/retry. No more hand-bundling secp256k1 to fit a single Worker module.
13
13
 
14
14
  At a glance: `Chain` (read/sign/send + RPC failover), `Relayer` (managed bot
15
- signing), `bindContracts` (call contracts by name), `Indexer` (typed subgraph
15
+ signing), `Wallet` (per-user custodial wallets, server-side custody),
16
+ `bindContracts` (call contracts by name), `Indexer` (typed subgraph
16
17
  queries), `Telegram` (webhook plumbing in one call), `Store` (D1 KV + idempotency),
17
18
  `requireSecret`, `queue`, `retry`, `log`.
18
19
 
@@ -46,7 +47,7 @@ keeper/bot/server that imports it ships a `package.json` declaring the dependenc
46
47
  audited `@noble` crypto — into one self-contained Worker module. No hand-bundling.
47
48
  - Targets with **no `package.json`** deploy exactly as before — the build step is
48
49
  opt-in by the presence of a manifest. Existing keepers keep working untouched.
49
- - Versions pin in `package.json` (`"@livo-build/runtime": "^0.2.2"`), so a runtime
50
+ - Versions pin in `package.json` (`"@livo-build/runtime": "^0.2.3"`), so a runtime
50
51
  update can't silently change a working keeper.
51
52
 
52
53
  Scaffold the common path with `create_keeper --template runtime-keeper`.
@@ -215,8 +216,7 @@ head" (new logs only); pass `fromBlock` to backfill.
215
216
  custodied **`RELAYER_PRIVATE_KEY`** (not the keeper key) and — when the platform
216
217
  injects `RELAYER_NONCE_URL` + `RELAYER_NONCE_TOKEN` — serializes nonces through
217
218
  Convex so concurrent bot invocations sharing one managed key don't collide
218
- (falling back to RPC pending otherwise). Scaffold with `create_bot --template
219
- relayer-bot`.
219
+ (falling back to RPC pending otherwise).
220
220
 
221
221
  ```ts
222
222
  import { Relayer, log } from "@livo-build/runtime";
@@ -224,6 +224,28 @@ const relayer = new Relayer(env); // RELAYER_PRIVATE_KEY
224
224
  const hash = await relayer.send({ address, abi, functionName, args });
225
225
  ```
226
226
 
227
+ ## Bots — `Wallet` (per-user custodial wallets)
228
+
229
+ `Wallet` gives each **end user** their own wallet(s). The bot signs on a user's
230
+ behalf **without ever holding key material**: every call hits the platform
231
+ (`WALLET_API_URL` + `WALLET_API_TOKEN`, auto-injected), which custodies a
232
+ per-`(project, user)` seed and signs server-side. Generated wallets derive from
233
+ the user's own seed (isolated from the project's platform mnemonic and from other
234
+ users); imported wallets store an encrypted raw key. This is the sanctioned
235
+ wallet primitive — don't hand-roll key storage. Scaffold with `create_bot
236
+ --template wallet-bot` (the default).
237
+
238
+ ```ts
239
+ import { Wallet } from "@livo-build/runtime";
240
+ const acct = new Wallet(env).user(msg.from.id); // a handle for one end-user
241
+ await acct.address(); // active wallet (auto-created on first call)
242
+ await acct.list(); // [{ label, address, active, imported }]
243
+ await acct.create("trading"); // new HD wallet from the user's own seed
244
+ await acct.import("0x<key>", "main"); // bring your own key
245
+ await acct.use("trading"); // switch active
246
+ const hash = await acct.send({ address, abi, functionName, args }); // platform signs
247
+ ```
248
+
227
249
  ## Queues
228
250
 
229
251
  Publish to a per-project queue from any deployable that declares `produces: [name]`
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ export type { WatchLogsOptions, WatchLogsResult } from "./watch.js";
7
7
  export { Relayer } from "./relayer.js";
8
8
  export { Queue, queue, queueEnvKey } from "./queue.js";
9
9
  export type { RelayerOptions } from "./relayer.js";
10
+ export { Wallet, UserWallet } from "./wallet.js";
11
+ export type { WalletOptions, WalletAccount, WalletSendOptions } from "./wallet.js";
10
12
  export { Indexer, indexer, indexerEnvKey, GraphQLError } from "./indexer.js";
11
13
  export type { IndexerOptions } from "./indexer.js";
12
14
  export { Telegram } from "./telegram.js";
package/dist/index.js CHANGED
@@ -11,6 +11,10 @@ export { watchLogs } from "./watch.js";
11
11
  // Convex-serialized nonces). A Chain that defaults to the relayer key.
12
12
  export { Relayer } from "./relayer.js";
13
13
  export { Queue, queue, queueEnvKey } from "./queue.js";
14
+ // Wallet — per-user CUSTODIAL wallets for bots. Each end-user owns wallet(s);
15
+ // the bot signs on their behalf via the platform (server-side custody, raw keys
16
+ // never reach the Worker). new Wallet(env).user(id).send({...}) / import / use.
17
+ export { Wallet, UserWallet } from "./wallet.js";
14
18
  // Indexer — typed GraphQL client for a project's Goldsky subgraph (reads the
15
19
  // stable `live`-alias endpoint injected as INDEXER_<NAME>_URL).
16
20
  export { Indexer, indexer, indexerEnvKey, GraphQLError } from "./indexer.js";
@@ -0,0 +1,77 @@
1
+ import { Chain } from "./chain.js";
2
+ import { type AbiFunction } from "./abi.js";
3
+ import type { Hex } from "./hex.js";
4
+ import type { Receipt } from "./chain.js";
5
+ interface MinimalEnv {
6
+ [key: string]: unknown;
7
+ }
8
+ export interface WalletOptions {
9
+ /** Platform wallet API base, e.g. https://<site>/wallet. Default env WALLET_API_URL. */
10
+ apiUrl?: string;
11
+ /** Bearer scoping calls to this bot. Default env WALLET_API_TOKEN. */
12
+ apiToken?: string;
13
+ }
14
+ /** One wallet a user owns. `imported` = brought their own key (vs. HD-generated). */
15
+ export interface WalletAccount {
16
+ label: string;
17
+ address: Hex;
18
+ active: boolean;
19
+ imported: boolean;
20
+ }
21
+ export interface WalletSendOptions {
22
+ address: Hex;
23
+ abi: AbiFunction[];
24
+ functionName: string;
25
+ args?: unknown[];
26
+ /** Native value to send with the call, in wei. */
27
+ value?: bigint;
28
+ }
29
+ /** A handle bound to a single end-user (e.g. one Telegram account). */
30
+ export declare class UserWallet {
31
+ private readonly wallet;
32
+ /** Platform-namespaced user id, e.g. "tg:12345". */
33
+ readonly userKey: string;
34
+ constructor(wallet: Wallet,
35
+ /** Platform-namespaced user id, e.g. "tg:12345". */
36
+ userKey: string);
37
+ /** Every wallet the user owns (auto-creates their default on first call). */
38
+ list(): Promise<WalletAccount[]>;
39
+ /** The user's active wallet (the one `send` signs with), auto-created if none. */
40
+ account(): Promise<WalletAccount>;
41
+ /** Shorthand for the active wallet's address. */
42
+ address(): Promise<Hex>;
43
+ /** Generate a new HD wallet (from the user's own seed) and make it active. */
44
+ create(label?: string): Promise<WalletAccount>;
45
+ /** Import an external private key and make it active. */
46
+ import(privateKey: string, label?: string): Promise<WalletAccount>;
47
+ /** Switch the active wallet by label. */
48
+ use(label: string): Promise<WalletAccount>;
49
+ /** Stop tracking a wallet by label (active reassigns to another if needed). */
50
+ remove(label: string): Promise<void>;
51
+ /** Native balance (wei) of the user's active wallet. Reads via RPC, no key. */
52
+ balance(): Promise<bigint>;
53
+ /**
54
+ * Sign + broadcast a state-changing call with the user's ACTIVE wallet. The
55
+ * calldata is encoded here; the platform signs server-side and returns the hash.
56
+ */
57
+ send(opts: WalletSendOptions): Promise<Hex>;
58
+ }
59
+ export declare class Wallet {
60
+ private readonly apiUrl?;
61
+ private readonly apiToken?;
62
+ private readonly env?;
63
+ private _chain?;
64
+ constructor(env: MinimalEnv | undefined, options?: WalletOptions);
65
+ /** A handle for one end-user. Pass any stable id; it's namespaced as "tg:<id>". */
66
+ user(id: string | number, platform?: string): UserWallet;
67
+ /** Lazily build a read-only Chain (for balances/receipts) from env RPC_URL. */
68
+ chain(): Chain;
69
+ /** Wait for a tx broadcast by `send` to mine. Reads via RPC, no key. */
70
+ waitForReceipt(hash: Hex, opts?: {
71
+ timeoutMs?: number;
72
+ pollMs?: number;
73
+ }): Promise<Receipt>;
74
+ /** Internal: POST to a wallet API sub-path with the bot bearer; throws on error. */
75
+ post(path: "accounts" | "sign", body: Record<string, unknown>): Promise<Record<string, unknown>>;
76
+ }
77
+ export {};
package/dist/wallet.js ADDED
@@ -0,0 +1,122 @@
1
+ // Wallet — per-user custodial wallets for bots. A bot's END USERS each get their
2
+ // own wallet(s); the bot signs on a user's behalf WITHOUT ever holding raw key
3
+ // material. Every management + signing call goes to the platform (WALLET_API_URL,
4
+ // authorized by WALLET_API_TOKEN, both injected at deploy); Convex resolves the
5
+ // user's active wallet and signs server-side, so a leaked Worker never leaks keys.
6
+ //
7
+ // const w = new Wallet(env);
8
+ // const acct = w.user(msg.from.id); // a handle scoped to one end-user
9
+ // await acct.address(); // their active wallet (auto-created)
10
+ // await acct.list(); // every wallet they own
11
+ // await acct.import("0x<key>", "trading"); // bring your own key
12
+ // await acct.use("trading"); // switch the active wallet
13
+ // const hash = await acct.send({ address, abi, functionName, args }); // platform signs
14
+ //
15
+ // Reads (balance, receipts) use the injected RPC_URL via a plain Chain — no key.
16
+ import { Chain } from "./chain.js";
17
+ import { encodeFunctionData, findFunction } from "./abi.js";
18
+ /** A handle bound to a single end-user (e.g. one Telegram account). */
19
+ export class UserWallet {
20
+ wallet;
21
+ userKey;
22
+ constructor(wallet,
23
+ /** Platform-namespaced user id, e.g. "tg:12345". */
24
+ userKey) {
25
+ this.wallet = wallet;
26
+ this.userKey = userKey;
27
+ }
28
+ /** Every wallet the user owns (auto-creates their default on first call). */
29
+ async list() {
30
+ const r = await this.wallet.post("accounts", { op: "list", user: this.userKey });
31
+ return (r.wallets ?? []);
32
+ }
33
+ /** The user's active wallet (the one `send` signs with), auto-created if none. */
34
+ async account() {
35
+ const r = await this.wallet.post("accounts", { op: "address", user: this.userKey });
36
+ return r.wallet;
37
+ }
38
+ /** Shorthand for the active wallet's address. */
39
+ async address() {
40
+ return (await this.account()).address;
41
+ }
42
+ /** Generate a new HD wallet (from the user's own seed) and make it active. */
43
+ async create(label) {
44
+ const r = await this.wallet.post("accounts", { op: "create", user: this.userKey, label });
45
+ return r.wallet;
46
+ }
47
+ /** Import an external private key and make it active. */
48
+ async import(privateKey, label) {
49
+ const r = await this.wallet.post("accounts", { op: "import", user: this.userKey, privateKey, label });
50
+ return r.wallet;
51
+ }
52
+ /** Switch the active wallet by label. */
53
+ async use(label) {
54
+ const r = await this.wallet.post("accounts", { op: "use", user: this.userKey, label });
55
+ return r.wallet;
56
+ }
57
+ /** Stop tracking a wallet by label (active reassigns to another if needed). */
58
+ async remove(label) {
59
+ await this.wallet.post("accounts", { op: "remove", user: this.userKey, label });
60
+ }
61
+ /** Native balance (wei) of the user's active wallet. Reads via RPC, no key. */
62
+ async balance() {
63
+ return this.wallet.chain().getBalance(await this.address());
64
+ }
65
+ /**
66
+ * Sign + broadcast a state-changing call with the user's ACTIVE wallet. The
67
+ * calldata is encoded here; the platform signs server-side and returns the hash.
68
+ */
69
+ async send(opts) {
70
+ const data = encodeFunctionData(findFunction(opts.abi, opts.functionName), opts.args ?? []);
71
+ const r = await this.wallet.post("sign", {
72
+ user: this.userKey,
73
+ to: opts.address,
74
+ data,
75
+ value: opts.value !== undefined ? `0x${opts.value.toString(16)}` : undefined,
76
+ });
77
+ return r.hash;
78
+ }
79
+ }
80
+ export class Wallet {
81
+ apiUrl;
82
+ apiToken;
83
+ env;
84
+ _chain;
85
+ constructor(env, options = {}) {
86
+ this.env = env;
87
+ this.apiUrl = (options.apiUrl ?? env?.WALLET_API_URL)?.replace(/\/$/, "");
88
+ this.apiToken = options.apiToken ?? env?.WALLET_API_TOKEN;
89
+ }
90
+ /** A handle for one end-user. Pass any stable id; it's namespaced as "tg:<id>". */
91
+ user(id, platform = "tg") {
92
+ const raw = String(id);
93
+ const userKey = /^[a-z]+:/.test(raw) ? raw : `${platform}:${raw}`;
94
+ return new UserWallet(this, userKey);
95
+ }
96
+ /** Lazily build a read-only Chain (for balances/receipts) from env RPC_URL. */
97
+ chain() {
98
+ if (!this._chain)
99
+ this._chain = new Chain(this.env);
100
+ return this._chain;
101
+ }
102
+ /** Wait for a tx broadcast by `send` to mine. Reads via RPC, no key. */
103
+ waitForReceipt(hash, opts) {
104
+ return this.chain().waitForReceipt(hash, opts);
105
+ }
106
+ /** Internal: POST to a wallet API sub-path with the bot bearer; throws on error. */
107
+ async post(path, body) {
108
+ if (!this.apiUrl || !this.apiToken) {
109
+ throw new Error("Wallet: not provisioned — WALLET_API_URL/WALLET_API_TOKEN missing (redeploy the bot so the platform injects them).");
110
+ }
111
+ const res = await fetch(`${this.apiUrl}/${path}`, {
112
+ method: "POST",
113
+ headers: { "content-type": "application/json", authorization: `Bearer ${this.apiToken}` },
114
+ body: JSON.stringify(body),
115
+ });
116
+ const j = (await res.json().catch(() => ({})));
117
+ if (!res.ok || j.error) {
118
+ throw new Error(`Wallet ${path}: ${String(j.message ?? j.error ?? `HTTP ${res.status}`)}`);
119
+ }
120
+ return j;
121
+ }
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livo-build/runtime",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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",