@livo-build/runtime 0.2.3 → 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
@@ -15,7 +15,7 @@ At a glance: `Chain` (read/sign/send + RPC failover), `Relayer` (managed bot
15
15
  signing), `Wallet` (per-user custodial wallets, server-side custody),
16
16
  `bindContracts` (call contracts by name), `Indexer` (typed subgraph
17
17
  queries), `Telegram` (webhook plumbing in one call), `Store` (D1 KV + idempotency),
18
- `requireSecret`, `queue`, `retry`, `log`.
18
+ `verifyMessage` (prove wallet ownership), `requireSecret`, `queue`, `retry`, `log`.
19
19
 
20
20
  ```ts
21
21
  import { Chain, Store, log } from "@livo-build/runtime";
@@ -246,6 +246,26 @@ await acct.use("trading"); // switch active
246
246
  const hash = await acct.send({ address, abi, functionName, args }); // platform signs
247
247
  ```
248
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
+
249
269
  ## Queues
250
270
 
251
271
  Publish to a per-project queue from any deployable that declares `produces: [name]`
package/dist/index.d.ts CHANGED
@@ -23,6 +23,8 @@ export { encodeFunctionData, encodeParameters, decodeParameters, decodeFunctionR
23
23
  export type { AbiFunction, AbiParameter, ParsedSignature } from "./abi.js";
24
24
  export { signTransaction, transactionHashToSign, transactionHash, privateKeyToAddress, toChecksumAddress, } from "./tx.js";
25
25
  export type { Eip1559Tx } from "./tx.js";
26
+ export { hashMessage, recoverMessageAddress, verifyMessage } from "./sig.js";
27
+ export type { VerifyMessageParams } from "./sig.js";
26
28
  export { eventTopic, encodeFilterTopics, encodeIndexedTopic, decodeLog, parseEventSignature, } from "./events.js";
27
29
  export type { AbiEvent, RawLog, DecodedLog } from "./events.js";
28
30
  export { bytesToHex, hexToBytes, concatBytes, toBigInt, } from "./hex.js";
package/dist/index.js CHANGED
@@ -28,6 +28,9 @@ export { requireSecret, requireSecrets, getSecret } from "./secret.js";
28
28
  // Low-level escape hatches — ABI coding, signing, RLP, hex. Power users only.
29
29
  export { encodeFunctionData, encodeParameters, decodeParameters, decodeFunctionResult, functionSelector, functionSignature, functionFromSignature, parseSignature, findFunction, keccak256, } from "./abi.js";
30
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";
31
34
  export { eventTopic, encodeFilterTopics, encodeIndexedTopic, decodeLog, parseEventSignature, } from "./events.js";
32
35
  export { bytesToHex, hexToBytes, concatBytes, toBigInt, } from "./hex.js";
33
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livo-build/runtime",
3
- "version": "0.2.3",
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",