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