@livo-build/runtime 0.2.2 → 0.2.4

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,9 +12,10 @@ 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
- `requireSecret`, `queue`, `retry`, `log`.
18
+ `verifyMessage` (prove wallet ownership), `requireSecret`, `queue`, `retry`, `log`.
18
19
 
19
20
  ```ts
20
21
  import { Chain, Store, log } from "@livo-build/runtime";
@@ -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,48 @@ 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
+
249
+ ## Prove wallet ownership — `verifyMessage`
250
+
251
+ Verify a "prove you own this wallet" signature **server-side** (EIP-191 `personal_sign`,
252
+ the basis of wallet-link / SIWE login) — no hand-rolled secp256k1. The pattern: a website
253
+ has the user connect a wallet and `personal_sign` a nonce; a bot/keeper then verifies it and
254
+ links the recovered address to the user's account.
255
+
256
+ ```ts
257
+ import { verifyMessage, recoverMessageAddress } from "@livo-build/runtime";
258
+
259
+ // true iff `address` produced `signature` over `message` (never throws)
260
+ if (verifyMessage({ address, message: "Link my wallet · nonce: " + nonce, signature })) {
261
+ // ownership proven — link `address` to the user
262
+ }
263
+ recoverMessageAddress(message, signature); // → the signer's checksummed address
264
+ ```
265
+
266
+ Scaffold the full flow (bot serves the connect+sign page, verifies, links, and gates a private
267
+ group; a keeper boots members who fall below) with `create_bot --template wallet-link-bot`.
268
+
227
269
  ## Queues
228
270
 
229
271
  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";
@@ -21,6 +23,8 @@ export { encodeFunctionData, encodeParameters, decodeParameters, decodeFunctionR
21
23
  export type { AbiFunction, AbiParameter, ParsedSignature } from "./abi.js";
22
24
  export { signTransaction, transactionHashToSign, transactionHash, privateKeyToAddress, toChecksumAddress, } from "./tx.js";
23
25
  export type { Eip1559Tx } from "./tx.js";
26
+ export { hashMessage, recoverMessageAddress, verifyMessage } from "./sig.js";
27
+ export type { VerifyMessageParams } from "./sig.js";
24
28
  export { eventTopic, encodeFilterTopics, encodeIndexedTopic, decodeLog, parseEventSignature, } from "./events.js";
25
29
  export type { AbiEvent, RawLog, DecodedLog } from "./events.js";
26
30
  export { bytesToHex, hexToBytes, concatBytes, toBigInt, } from "./hex.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";
@@ -24,6 +28,9 @@ export { requireSecret, requireSecrets, getSecret } from "./secret.js";
24
28
  // Low-level escape hatches — ABI coding, signing, RLP, hex. Power users only.
25
29
  export { encodeFunctionData, encodeParameters, decodeParameters, decodeFunctionResult, functionSelector, functionSignature, functionFromSignature, parseSignature, findFunction, keccak256, } from "./abi.js";
26
30
  export { signTransaction, transactionHashToSign, transactionHash, privateKeyToAddress, toChecksumAddress, } from "./tx.js";
31
+ // EIP-191 message verification — prove a user controls an address from a signed
32
+ // message (wallet-link / SIWE login), recovered server-side with no hand-rolled crypto.
33
+ export { hashMessage, recoverMessageAddress, verifyMessage } from "./sig.js";
27
34
  export { eventTopic, encodeFilterTopics, encodeIndexedTopic, decodeLog, parseEventSignature, } from "./events.js";
28
35
  export { bytesToHex, hexToBytes, concatBytes, toBigInt, } from "./hex.js";
29
36
  // Layer 2 — generated contract bindings live in "@livo/runtime/contracts".
package/dist/sig.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { type Bytes, type Hex } from "./hex.js";
2
+ /**
3
+ * The EIP-191 digest a wallet signs for `personal_sign(message)`:
4
+ * keccak256("\x19Ethereum Signed Message:\n" + byteLength(message) + message)
5
+ * `message` is treated as UTF-8 text (the common case for login/link flows).
6
+ */
7
+ export declare function hashMessage(message: string): Bytes;
8
+ /**
9
+ * Recover the EIP-55 checksummed address that produced `signature` over
10
+ * `message` (an EIP-191 personal_sign). `signature` is the 65-byte 0x string
11
+ * r(32) ++ s(32) ++ v(1), where v is 27/28 (or 0/1). Throws on a malformed sig.
12
+ */
13
+ export declare function recoverMessageAddress(message: string, signature: Hex | string): Hex;
14
+ export interface VerifyMessageParams {
15
+ /** The address you expect signed the message. */
16
+ address: Hex | string;
17
+ /** The exact message string that was signed. */
18
+ message: string;
19
+ /** The 65-byte 0x signature returned by the wallet. */
20
+ signature: Hex | string;
21
+ }
22
+ /**
23
+ * True iff `signature` over `message` was produced by `address` — the safe way to
24
+ * verify "prove you own this wallet" server-side. Never throws: a malformed
25
+ * signature returns false.
26
+ */
27
+ export declare function verifyMessage({ address, message, signature }: VerifyMessageParams): boolean;
package/dist/sig.js ADDED
@@ -0,0 +1,50 @@
1
+ // EIP-191 personal_sign message verification — prove a user controls an address
2
+ // from a signature, with zero hand-rolled crypto. The counterpart to tx.ts's
3
+ // signing: here we RECOVER the signer of a `personal_sign` message (what
4
+ // wallets produce for `eth_sign`/`personal_sign` and SIWE login). secp256k1
5
+ // recovery via noble; same keccak + hex helpers as the rest of the runtime.
6
+ import { secp256k1 } from "@noble/curves/secp256k1";
7
+ import { keccak_256 } from "@noble/hashes/sha3";
8
+ import { bytesToHex, concatBytes, hexToBytes } from "./hex.js";
9
+ import { toChecksumAddress } from "./tx.js";
10
+ const PERSONAL_PREFIX = "\x19Ethereum Signed Message:\n";
11
+ /**
12
+ * The EIP-191 digest a wallet signs for `personal_sign(message)`:
13
+ * keccak256("\x19Ethereum Signed Message:\n" + byteLength(message) + message)
14
+ * `message` is treated as UTF-8 text (the common case for login/link flows).
15
+ */
16
+ export function hashMessage(message) {
17
+ const body = new TextEncoder().encode(message);
18
+ const prefix = new TextEncoder().encode(PERSONAL_PREFIX + body.length);
19
+ return keccak_256(concatBytes(prefix, body));
20
+ }
21
+ /**
22
+ * Recover the EIP-55 checksummed address that produced `signature` over
23
+ * `message` (an EIP-191 personal_sign). `signature` is the 65-byte 0x string
24
+ * r(32) ++ s(32) ++ v(1), where v is 27/28 (or 0/1). Throws on a malformed sig.
25
+ */
26
+ export function recoverMessageAddress(message, signature) {
27
+ const bytes = hexToBytes(signature);
28
+ if (bytes.length !== 65)
29
+ throw new Error("signature must be 65 bytes (r,s,v)");
30
+ const v = bytes[64];
31
+ const recovery = v >= 27 ? v - 27 : v;
32
+ if (recovery !== 0 && recovery !== 1)
33
+ throw new Error(`invalid signature recovery byte: ${v}`);
34
+ const sig = secp256k1.Signature.fromCompact(bytes.slice(0, 64)).addRecoveryBit(recovery);
35
+ const pub = sig.recoverPublicKey(hashMessage(message)).toRawBytes(false).slice(1); // drop 0x04
36
+ return toChecksumAddress(bytesToHex(keccak_256(pub).slice(-20)));
37
+ }
38
+ /**
39
+ * True iff `signature` over `message` was produced by `address` — the safe way to
40
+ * verify "prove you own this wallet" server-side. Never throws: a malformed
41
+ * signature returns false.
42
+ */
43
+ export function verifyMessage({ address, message, signature }) {
44
+ try {
45
+ return recoverMessageAddress(message, signature).toLowerCase() === address.toLowerCase();
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
@@ -54,6 +54,14 @@ export declare class Telegram {
54
54
  handleUpdate(req: Request, reply: (msg: BotMessage) => string | null | undefined | Promise<string | null | undefined>, opts?: SendMessageOptions): Promise<Response>;
55
55
  /** Send a message to a chat. No-op-safe: throws if no token is configured. */
56
56
  sendMessage(chatId: number | string, text: string, opts?: SendMessageOptions): Promise<void>;
57
+ /**
58
+ * Send a photo to a chat. `photo` is a URL Telegram fetches, or a Telegram
59
+ * file_id. (To upload raw bytes, post multipart/form-data to sendPhoto directly.)
60
+ */
61
+ sendPhoto(chatId: number | string, photo: string, opts?: {
62
+ caption?: string;
63
+ parseMode?: "Markdown" | "MarkdownV2" | "HTML";
64
+ }): Promise<void>;
57
65
  /** Register the slash-command menu (setMyCommands) — autocompletes in chat. */
58
66
  setMyCommands(commands: Array<{
59
67
  command: string;
package/dist/telegram.js CHANGED
@@ -102,6 +102,26 @@ export class Telegram {
102
102
  if (!res.ok)
103
103
  throw new Error(`Telegram sendMessage failed: HTTP ${res.status}`);
104
104
  }
105
+ /**
106
+ * Send a photo to a chat. `photo` is a URL Telegram fetches, or a Telegram
107
+ * file_id. (To upload raw bytes, post multipart/form-data to sendPhoto directly.)
108
+ */
109
+ async sendPhoto(chatId, photo, opts = {}) {
110
+ if (!this.token)
111
+ throw new Error("Telegram: no bot token (set_secret BOT_TOKEN=<token>).");
112
+ const body = { chat_id: chatId, photo };
113
+ if (opts.caption)
114
+ body.caption = opts.caption;
115
+ if (opts.parseMode)
116
+ body.parse_mode = opts.parseMode;
117
+ const res = await fetch(`${API}${this.token}/sendPhoto`, {
118
+ method: "POST",
119
+ headers: { "content-type": "application/json" },
120
+ body: JSON.stringify(body),
121
+ });
122
+ if (!res.ok)
123
+ throw new Error(`Telegram sendPhoto failed: HTTP ${res.status}`);
124
+ }
105
125
  /** Register the slash-command menu (setMyCommands) — autocompletes in chat. */
106
126
  async setMyCommands(commands) {
107
127
  if (!this.token)
@@ -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.4",
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",