@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 +21 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/sig.d.ts +27 -0
- package/dist/sig.js +50 -0
- package/dist/telegram.d.ts +8 -0
- package/dist/telegram.js +20 -0
- package/package.json +1 -1
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
|
+
}
|
package/dist/telegram.d.ts
CHANGED
|
@@ -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)
|