@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 +47 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -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/dist/wallet.d.ts +77 -0
- package/dist/wallet.js +122 -0
- package/package.json +1 -1
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), `
|
|
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.
|
|
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).
|
|
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
|
+
}
|
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)
|
package/dist/wallet.d.ts
ADDED
|
@@ -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
|
+
}
|