@lunardraw/agent 0.1.0 → 0.1.1
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/node_modules/@lunardraw/cli/dist/kit/directory.js +55 -0
- package/node_modules/@lunardraw/cli/dist/kit/home.js +6 -1
- package/node_modules/@lunardraw/cli/dist/kit/main.js +51 -0
- package/node_modules/@lunardraw/cli/dist/kit/mcp.js +1 -1
- package/node_modules/@lunardraw/cli/dist/kit/setup.js +25 -0
- package/node_modules/@lunardraw/cli/dist/kit/tools.js +15 -0
- package/node_modules/@lunardraw/cli/dist/listing.js +157 -0
- package/node_modules/@lunardraw/cli/dist/registry-main.js +39 -0
- package/node_modules/@lunardraw/cli/dist/registry.js +331 -0
- package/node_modules/@lunardraw/cli/package.json +3 -3
- package/package.json +2 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Listing the agent in lunardraw.io's agent directory, if its owner opted in
|
|
3
|
+
* (`lunardraw-agent list`). Reports are signed with the agent wallet's key and
|
|
4
|
+
* carry its name, style and totals, never its numbers. See ../listing.ts.
|
|
5
|
+
*/
|
|
6
|
+
import { Starfall } from '@lunardraw/contract';
|
|
7
|
+
import { agentReport, signedListing, VIAS, walletSigner } from '../listing.js';
|
|
8
|
+
import { loadAgentState } from '../starfall-agent.js';
|
|
9
|
+
import { DEFAULT_REGISTRY, kitPaths } from './home.js';
|
|
10
|
+
const { DrawStatus } = Starfall;
|
|
11
|
+
/** The chain view's draws as outcomes (numbers and prizes). */
|
|
12
|
+
export const outcomesOf = (view) => view.draws.map((d) => ({
|
|
13
|
+
num: Number(d.id),
|
|
14
|
+
status: d.status === DrawStatus.voided ? 'void' : d.status === DrawStatus.settled ? 'settled' : d.status === DrawStatus.drawn ? 'drawn' : 'open',
|
|
15
|
+
...(d.status === DrawStatus.drawn || d.status === DrawStatus.settled ? { numbers: d.numbers, moonball: d.moonball } : {}),
|
|
16
|
+
...(d.status === DrawStatus.settled ? { prizes_star: d.prizes.map(Number) } : {}),
|
|
17
|
+
}));
|
|
18
|
+
/** Which AI an owner names (--via), checked. */
|
|
19
|
+
export const viaOf = (v) => {
|
|
20
|
+
const via = (v ?? 'other').toLowerCase();
|
|
21
|
+
if (!VIAS.includes(via))
|
|
22
|
+
throw new Error(`--via must be one of ${VIAS.join(', ')}`);
|
|
23
|
+
return via;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Send a listing report (or remove the listing). `view` gives the draws'
|
|
27
|
+
* outcomes for the totals; without it, the totals count purchases only.
|
|
28
|
+
*/
|
|
29
|
+
export const sendListing = async (config, seed, action, o = {}) => {
|
|
30
|
+
const listing = o.listing ?? config.listing;
|
|
31
|
+
if (!listing)
|
|
32
|
+
throw new Error('this agent isn\'t listed: run `lunardraw-agent list --name <name>` first');
|
|
33
|
+
const state = loadAgentState(kitPaths(o.home, config.network).state);
|
|
34
|
+
const report = agentReport(state, o.view ? outcomesOf(o.view) : [], Date.now());
|
|
35
|
+
const signer = await walletSigner(seed, config.network);
|
|
36
|
+
const body = signedListing(signer, { network: config.network, action, name: listing.name, style: listing.style, via: viaOf(listing.via), ...report });
|
|
37
|
+
const url = `${(config.registry ?? DEFAULT_REGISTRY).replace(/\/$/, '')}/report`;
|
|
38
|
+
const r = await (o.fetcher ?? fetch)(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(20_000) });
|
|
39
|
+
const answer = (await r.json().catch(() => ({})));
|
|
40
|
+
if (!r.ok || !answer.ok)
|
|
41
|
+
throw new Error(`the directory refused it: ${answer.error ?? `HTTP ${r.status}`}`);
|
|
42
|
+
return answer.result ?? 'ok';
|
|
43
|
+
};
|
|
44
|
+
/** Best effort, for after a purchase or now and then: a failed report never stops play. */
|
|
45
|
+
export const refreshListing = async (config, seed, o = {}) => {
|
|
46
|
+
if (!config.listing)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
await sendListing(config, seed, 'list', o);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
o.log?.(`(couldn't update the directory listing: ${err instanceof Error ? err.message : String(err)})`);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=directory.js.map
|
|
@@ -15,6 +15,8 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSy
|
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
16
|
import { dirname, join, resolve } from 'node:path';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
+
/** Lunardraw's agent registry (the agent directory on lunardraw.io reads it). */
|
|
19
|
+
export const DEFAULT_REGISTRY = 'https://207-180-249-120.sslip.io/agents';
|
|
18
20
|
export const DEFAULT_CONFIG = { network: 'preprod', prover: 'hosted', limits: { perDraw: 25, perDay: 100 } };
|
|
19
21
|
export const kitHome = () => process.env.LUNARDRAW_AGENT_HOME ?? join(homedir(), '.lunardraw-agent');
|
|
20
22
|
export const kitPaths = (home = kitHome(), network = 'preprod') => ({
|
|
@@ -46,7 +48,10 @@ export const parseConfig = (raw) => {
|
|
|
46
48
|
if (typeof prover !== 'string' || !(prover === 'hosted' || /^https?:\/\/\S+$/.test(prover) || /^local:\d+$/.test(prover))) {
|
|
47
49
|
throw new Error('config.json: prover must be "hosted", a prover\'s https:// address, or "local:<port>"');
|
|
48
50
|
}
|
|
49
|
-
|
|
51
|
+
const listing = c.listing && typeof c.listing === 'object' ? { name: String(c.listing.name ?? ''), style: String(c.listing.style ?? ''), via: String(c.listing.via ?? 'other') } : undefined;
|
|
52
|
+
if (c.registry !== undefined && (typeof c.registry !== 'string' || !/^https?:\/\/\S+$/.test(c.registry)))
|
|
53
|
+
throw new Error('config.json: registry must be an https:// address');
|
|
54
|
+
return { network, prover, limits, ...(c.payTo ? { payTo: String(c.payTo) } : {}), ...(listing ? { listing } : {}), ...(c.registry ? { registry: c.registry } : {}) };
|
|
50
55
|
};
|
|
51
56
|
export const loadConfig = (home = kitHome()) => {
|
|
52
57
|
const path = kitPaths(home).config;
|
|
@@ -18,6 +18,10 @@ export const KIT_USAGE = `lunardraw-agent: play Starfall, Lunardraw's lottery on
|
|
|
18
18
|
lunardraw-agent play [--style mix|quickpick|lucky|moonhit|system] [--games <n>] [--numbers ... --moonball <n>]
|
|
19
19
|
keep playing every draw, within the limits
|
|
20
20
|
lunardraw-agent limits the owner's spending limits (edit config.json to change)
|
|
21
|
+
lunardraw-agent list --name <name> [--style "<few words>"] [--via claude|gemini|openclaw|hermes|mcp|cli|other]
|
|
22
|
+
show this agent in lunardraw.io's agent directory (its name, style
|
|
23
|
+
and totals, signed by its wallet; never its numbers)
|
|
24
|
+
lunardraw-agent unlist take it out of the directory
|
|
21
25
|
lunardraw-agent mcp run as an MCP server for your AI (Claude, Gemini, OpenClaw, Hermes...)
|
|
22
26
|
|
|
23
27
|
--json prints machine-readable output (status, wallet, buy, tickets).
|
|
@@ -59,6 +63,8 @@ export const kit = async (argv) => {
|
|
|
59
63
|
pick: { type: 'string' },
|
|
60
64
|
sweep: { type: 'boolean', default: false },
|
|
61
65
|
style: { type: 'string' },
|
|
66
|
+
name: { type: 'string' },
|
|
67
|
+
via: { type: 'string' },
|
|
62
68
|
help: { type: 'boolean', default: false },
|
|
63
69
|
},
|
|
64
70
|
});
|
|
@@ -149,6 +155,10 @@ export const kit = async (argv) => {
|
|
|
149
155
|
throw new Error('no draw is on sale right now; the next opens on the hour');
|
|
150
156
|
return withPlayer(config, seed, async (p) => {
|
|
151
157
|
const outcome = await p.buy(entries, (m) => p.session.logger.info(m));
|
|
158
|
+
if (outcome.bought && config.listing) {
|
|
159
|
+
const { refreshListing } = await import('./directory.js');
|
|
160
|
+
await refreshListing(config, seed, { view: (await readOverview(config).catch(() => undefined))?.view, log: (m) => process.stderr.write(m + '\n') });
|
|
161
|
+
}
|
|
152
162
|
if (values.json)
|
|
153
163
|
return json(outcome), outcome.bought === outcome.of ? 0 : 1;
|
|
154
164
|
if (outcome.bought) {
|
|
@@ -176,6 +186,10 @@ export const kit = async (argv) => {
|
|
|
176
186
|
p.tickets.forEach((t) => say(` ${t}`));
|
|
177
187
|
}
|
|
178
188
|
say(`Spent in the last 24 hours: ${report.spentLast24h}.`);
|
|
189
|
+
if (config.listing && view) {
|
|
190
|
+
const { refreshListing } = await import('./directory.js');
|
|
191
|
+
await refreshListing(config, ensureWallet().seed, { view, log: (m) => process.stderr.write(m + '\n') });
|
|
192
|
+
}
|
|
179
193
|
return 0;
|
|
180
194
|
}
|
|
181
195
|
case 'play': {
|
|
@@ -199,8 +213,45 @@ export const kit = async (argv) => {
|
|
|
199
213
|
...(values.numbers ? ['--numbers', values.numbers] : []),
|
|
200
214
|
...(values.moonball ? ['--moonball', values.moonball] : []),
|
|
201
215
|
];
|
|
216
|
+
if (config.listing) {
|
|
217
|
+
const { refreshListing } = await import('./directory.js');
|
|
218
|
+
const { readOverview } = await import('./player.js');
|
|
219
|
+
const seed = ensureWallet().seed;
|
|
220
|
+
const tick = async () => refreshListing(config, seed, { view: (await readOverview(config).catch(() => undefined))?.view });
|
|
221
|
+
void tick();
|
|
222
|
+
setInterval(() => void tick(), 30 * 60_000).unref();
|
|
223
|
+
}
|
|
202
224
|
return starfall(args);
|
|
203
225
|
}
|
|
226
|
+
case 'list': {
|
|
227
|
+
const { config, seed } = setup();
|
|
228
|
+
const name = values.name ?? config.listing?.name;
|
|
229
|
+
if (!name)
|
|
230
|
+
throw new Error('give the agent a name: lunardraw-agent list --name <name> (2-20 letters, digits, spaces, . _ -)');
|
|
231
|
+
const { sendListing, viaOf } = await import('./directory.js');
|
|
232
|
+
const { readOverview } = await import('./player.js');
|
|
233
|
+
const listing = { name, style: values.style ?? config.listing?.style ?? '', via: viaOf(values.via ?? config.listing?.via) };
|
|
234
|
+
const view = (await readOverview(config).catch(() => undefined))?.view;
|
|
235
|
+
const result = await sendListing(config, seed, 'list', { listing, view });
|
|
236
|
+
saveConfig({ ...config, listing }, kitHome());
|
|
237
|
+
say(`${result === 'listed' ? 'Listed' : 'Updated'}: ${name} is in the agent directory on https://lunardraw.io/agents/ (it updates after each purchase).`);
|
|
238
|
+
say('It shows the name, style and totals (draws, games, tNIGHT spent and won), never the numbers. `lunardraw-agent unlist` takes it out.');
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
case 'unlist': {
|
|
242
|
+
const { config, seed } = setup();
|
|
243
|
+
if (!config.listing) {
|
|
244
|
+
say('This agent isn\'t listed.');
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
const { sendListing } = await import('./directory.js');
|
|
248
|
+
await sendListing(config, seed, 'unlist');
|
|
249
|
+
const rest = { ...config };
|
|
250
|
+
delete rest.listing;
|
|
251
|
+
saveConfig(rest, kitHome());
|
|
252
|
+
say(`${config.listing.name} is out of the agent directory.`);
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
204
255
|
case 'mcp': {
|
|
205
256
|
process.env.NIGHTCAST_LOG_FD = '2';
|
|
206
257
|
const { mcpTools } = await import('./tools.js');
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { createInterface } from 'node:readline';
|
|
14
14
|
import { Starfall } from '@lunardraw/contract';
|
|
15
|
-
export const SERVER_INFO = { name: 'lunardraw', title: 'Lunardraw', version: '0.1.
|
|
15
|
+
export const SERVER_INFO = { name: 'lunardraw', title: 'Lunardraw', version: '0.1.1' };
|
|
16
16
|
const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
17
17
|
export const INSTRUCTIONS = 'Lunardraw runs Starfall, a lottery on the Midnight Preprod test network (tNIGHT test tokens with no value). ' +
|
|
18
18
|
'Draws close on the hour on Preprod. A game is 7 numbers from 1-35 plus a Moonball from 1-20 and costs 1.2 tNIGHT; prizes and refunds ' +
|
|
@@ -198,6 +198,31 @@ export const setup = async () => {
|
|
|
198
198
|
say(' (Allow tool calls up to 15 minutes: the first purchase opens the wallet, then buys.)');
|
|
199
199
|
say('ChatGPT and Grok can use any of these agents as the brain (OpenClaw and Hermes run GPT, Grok and Gemini models);');
|
|
200
200
|
say("their own apps only reach MCP servers on the internet, which this one (holding your wallet) deliberately isn't.");
|
|
201
|
+
// the agent directory (opt in)
|
|
202
|
+
if (!config.listing && (await yes('\nShow your agent in the agent directory on lunardraw.io? It shows a name you choose and its totals, never its numbers.'))) {
|
|
203
|
+
const { NAME_RULE, VIAS } = await import('../listing.js');
|
|
204
|
+
let name = '';
|
|
205
|
+
while (!NAME_RULE.test(name)) {
|
|
206
|
+
name = (await rl.question('Its name (2-20 letters, digits, spaces, . _ -; Enter to skip): ')).trim();
|
|
207
|
+
if (!name)
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (name) {
|
|
211
|
+
const via = (await rl.question(`Which AI plays it? (${VIAS.join(', ')}) [other]: `)).trim().toLowerCase() || 'other';
|
|
212
|
+
const style = (await rl.question('Its style in a few words (optional, e.g. "Plays every night"): ')).trim().slice(0, 40);
|
|
213
|
+
try {
|
|
214
|
+
const { sendListing, viaOf } = await import('./directory.js');
|
|
215
|
+
const listing = { name, style, via: viaOf(via) };
|
|
216
|
+
await sendListing(config, seed, 'list', { home, listing });
|
|
217
|
+
config = { ...config, listing };
|
|
218
|
+
saveConfig(config, home);
|
|
219
|
+
say(`✓ ${name} is in the directory: https://lunardraw.io/agents/ (\`lunardraw-agent unlist\` takes it out).`);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
say(`Couldn't list it now (${err instanceof Error ? err.message : String(err)}); try later with: lunardraw-agent list --name "${name}"`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
201
226
|
// 4. sync
|
|
202
227
|
step(4, 'sync the wallet');
|
|
203
228
|
say('The wallet now reads the Preprod chain. The first time takes hours; leave this window open.');
|
|
@@ -7,6 +7,7 @@ import { loadAgentState } from '../starfall-agent.js';
|
|
|
7
7
|
import { buyEntries } from '../starfall-commands.js';
|
|
8
8
|
import { ensureWallet, hasBlockfrost, kitHome, kitPaths, loadConfig, useHome } from './home.js';
|
|
9
9
|
import { BUY_SCHEMA, buyArgs } from './mcp.js';
|
|
10
|
+
import { refreshListing } from './directory.js';
|
|
10
11
|
import { readOverview, ticketsReport, withPlayer } from './player.js';
|
|
11
12
|
const DIVISIONS = ['7 + Moonball', '7', '6 + Moonball', '6', '5 + Moonball', '4 + Moonball', '5', '3 + Moonball', '2 + Moonball'];
|
|
12
13
|
export const mcpTools = () => {
|
|
@@ -44,6 +45,18 @@ export const mcpTools = () => {
|
|
|
44
45
|
if (process.env.LUNARDRAW_AGENT_LAZY !== '1' && hasBlockfrost(kitHome(), config.network)) {
|
|
45
46
|
setTimeout(() => void openPlayer(() => undefined).catch(() => undefined), 0).unref?.();
|
|
46
47
|
}
|
|
48
|
+
// If the owner listed the agent in lunardraw.io's directory, keep it up to date
|
|
49
|
+
// (and shown as online) while the server runs, and after each purchase.
|
|
50
|
+
const listingTick = async () => {
|
|
51
|
+
if (!config.listing || !hasBlockfrost(kitHome(), config.network))
|
|
52
|
+
return;
|
|
53
|
+
const view = (await readOverview(config).catch(() => undefined))?.view;
|
|
54
|
+
await refreshListing(config, ensureWallet().seed, { view, log: (m) => process.stderr.write(m + '\n') });
|
|
55
|
+
};
|
|
56
|
+
if (config.listing) {
|
|
57
|
+
setTimeout(() => void listingTick(), 5_000).unref?.();
|
|
58
|
+
setInterval(() => void listingTick(), 30 * 60_000).unref?.();
|
|
59
|
+
}
|
|
47
60
|
// keep the client informed during long calls
|
|
48
61
|
const ticking = async (progress, what, work) => {
|
|
49
62
|
const t = setInterval(() => progress(`${what}…`), 15_000);
|
|
@@ -109,6 +122,8 @@ export const mcpTools = () => {
|
|
|
109
122
|
const entries = buyEntries(buyArgs(args));
|
|
110
123
|
const p = await ticking(progress, 'opening the wallet', openPlayer(progress));
|
|
111
124
|
const outcome = await ticking(progress, 'buying', p.buy(entries, progress));
|
|
125
|
+
if (outcome.bought)
|
|
126
|
+
void listingTick();
|
|
112
127
|
return { text: JSON.stringify(outcome, null, 2), isError: outcome.bought < outcome.of };
|
|
113
128
|
},
|
|
114
129
|
},
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent listings for lunardraw.io's agent directory.
|
|
3
|
+
*
|
|
4
|
+
* An agent's owner can list it (opt in): the agent then reports its name,
|
|
5
|
+
* style and totals, signed with its wallet's unshielded key, so a listing can
|
|
6
|
+
* only come from the wallet it names. The totals are the agent's own record
|
|
7
|
+
* of what it bought (tickets are private on chain, so no one else can count
|
|
8
|
+
* them); prizes are counted from the drawn numbers and each division's
|
|
9
|
+
* published prize. The agent's numbers are never sent.
|
|
10
|
+
*
|
|
11
|
+
* Lunardraw's own agents (Nova, Orbit, Pixel on the keeper VM) are read by the
|
|
12
|
+
* registry from their state files directly, with the same `agentStats`.
|
|
13
|
+
*/
|
|
14
|
+
import { Starfall } from '@lunardraw/contract';
|
|
15
|
+
export const emptyStats = () => ({ draws: 0, games: 0, spent_star: 0, winning_games: 0, won_star: 0 });
|
|
16
|
+
/**
|
|
17
|
+
* The start of this week (Monday 00:00) or month (the 1st) on Brisbane time,
|
|
18
|
+
* UTC+10 all year: the leaderboard's periods.
|
|
19
|
+
*/
|
|
20
|
+
export const periodStart = (period, nowMs) => {
|
|
21
|
+
const OFFSET = 10 * 3_600_000;
|
|
22
|
+
const local = new Date(nowMs + OFFSET);
|
|
23
|
+
const y = local.getUTCFullYear();
|
|
24
|
+
const m = local.getUTCMonth();
|
|
25
|
+
const d = local.getUTCDate();
|
|
26
|
+
const startLocal = period === 'month' ? Date.UTC(y, m, 1) : Date.UTC(y, m, d) - ((local.getUTCDay() + 6) % 7) * 86_400_000;
|
|
27
|
+
return new Date(startLocal - OFFSET).toISOString();
|
|
28
|
+
};
|
|
29
|
+
/** An agent's totals from its own purchase record and the draws' outcomes (purchases from `since` on, if given). */
|
|
30
|
+
export const agentStats = (state, draws, since) => {
|
|
31
|
+
const byNum = new Map(draws.map((d) => [d.num, d]));
|
|
32
|
+
const s = emptyStats();
|
|
33
|
+
const played = new Set();
|
|
34
|
+
for (const p of state.purchases) {
|
|
35
|
+
if (since && p.at < since)
|
|
36
|
+
continue;
|
|
37
|
+
const num = Number(p.draw);
|
|
38
|
+
const d = byNum.get(num);
|
|
39
|
+
if (d?.status === 'void')
|
|
40
|
+
continue; // refunded
|
|
41
|
+
played.add(num);
|
|
42
|
+
s.games += p.games;
|
|
43
|
+
s.spent_star += Number(p.cost);
|
|
44
|
+
s.last_draw = Math.max(s.last_draw ?? 0, num);
|
|
45
|
+
if (!s.first_played || p.at < s.first_played)
|
|
46
|
+
s.first_played = p.at;
|
|
47
|
+
if (!s.last_played || p.at > s.last_played)
|
|
48
|
+
s.last_played = p.at;
|
|
49
|
+
if (!d?.numbers || d.moonball === undefined || !d.numbers.some((n) => n > 0))
|
|
50
|
+
continue;
|
|
51
|
+
for (const e of p.entries) {
|
|
52
|
+
const won = Starfall.gamesByDivisionFor(e, d.numbers, d.moonball);
|
|
53
|
+
won.forEach((games, i) => {
|
|
54
|
+
if (!games)
|
|
55
|
+
return;
|
|
56
|
+
s.winning_games += Number(games);
|
|
57
|
+
if (d.prizes_star?.[i])
|
|
58
|
+
s.won_star += Number(games) * d.prizes_star[i];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
s.draws = played.size;
|
|
63
|
+
return s;
|
|
64
|
+
};
|
|
65
|
+
/** Every draw the agent won something in (settled draws), newest first. */
|
|
66
|
+
export const agentWins = (state, draws) => {
|
|
67
|
+
const byNum = new Map(draws.map((d) => [d.num, d]));
|
|
68
|
+
const perDraw = new Map();
|
|
69
|
+
for (const p of state.purchases) {
|
|
70
|
+
const num = Number(p.draw);
|
|
71
|
+
const d = byNum.get(num);
|
|
72
|
+
if (!d?.numbers || d.moonball === undefined || !d.prizes_star || d.status === 'void')
|
|
73
|
+
continue;
|
|
74
|
+
for (const e of p.entries) {
|
|
75
|
+
Starfall.gamesByDivisionFor(e, d.numbers, d.moonball).forEach((g, i) => {
|
|
76
|
+
if (!g)
|
|
77
|
+
return;
|
|
78
|
+
const w = perDraw.get(num) ?? { draw: num, division: i + 1, games: 0, won_star: 0 };
|
|
79
|
+
w.division = Math.min(w.division, i + 1);
|
|
80
|
+
w.games += Number(g);
|
|
81
|
+
w.won_star += Number(g) * (d.prizes_star[i] ?? 0);
|
|
82
|
+
perDraw.set(num, w);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...perDraw.values()].sort((a, b) => b.draw - a.draw);
|
|
87
|
+
};
|
|
88
|
+
export const agentReport = (state, draws, nowMs) => {
|
|
89
|
+
const week = periodStart('week', nowMs);
|
|
90
|
+
const month = periodStart('month', nowMs);
|
|
91
|
+
return {
|
|
92
|
+
stats: agentStats(state, draws),
|
|
93
|
+
week: { ...agentStats(state, draws, week), since: week },
|
|
94
|
+
month: { ...agentStats(state, draws, month), since: month },
|
|
95
|
+
wins: agentWins(state, draws).slice(0, 20),
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
// ------------------------------------------------------------------ the signed listing
|
|
99
|
+
export const LISTING_PREFIX = 'lunardraw-agent-listing\n';
|
|
100
|
+
/** How an agent plays (for the directory's "via"). */
|
|
101
|
+
export const VIAS = ['claude', 'gemini', 'openclaw', 'hermes', 'mcp', 'cli', 'other'];
|
|
102
|
+
export const NAME_RULE = /^[A-Za-z0-9][A-Za-z0-9 ._-]{1,19}$/;
|
|
103
|
+
/** Why a name, style or via can't be listed, or undefined. */
|
|
104
|
+
export const listingProblem = (p) => {
|
|
105
|
+
if (!NAME_RULE.test(p.name))
|
|
106
|
+
return 'the name must be 2 to 20 letters, digits, spaces, dots, dashes or underscores, starting with a letter or digit';
|
|
107
|
+
if (p.style.length > 40 || /[\u0000-\u001f<>]/.test(p.style))
|
|
108
|
+
return 'the style must be up to 40 characters, with no < or >';
|
|
109
|
+
if (!VIAS.includes(p.via))
|
|
110
|
+
return `via must be one of ${VIAS.join(', ')}`;
|
|
111
|
+
return undefined;
|
|
112
|
+
};
|
|
113
|
+
/** The bytes that are signed: a fixed prefix and the payload exactly as sent. */
|
|
114
|
+
export const listingMessage = (payload) => new TextEncoder().encode(LISTING_PREFIX + payload);
|
|
115
|
+
/** A signed listing report, ready to POST to the registry. */
|
|
116
|
+
export const signedListing = (signer, p, now = new Date()) => {
|
|
117
|
+
const problem = listingProblem(p);
|
|
118
|
+
if (problem)
|
|
119
|
+
throw new Error(problem);
|
|
120
|
+
const payload = JSON.stringify({
|
|
121
|
+
v: 1,
|
|
122
|
+
network: p.network,
|
|
123
|
+
key: signer.key,
|
|
124
|
+
action: p.action,
|
|
125
|
+
name: p.name,
|
|
126
|
+
style: p.style,
|
|
127
|
+
via: p.via,
|
|
128
|
+
stats: p.stats,
|
|
129
|
+
...(p.week ? { week: p.week } : {}),
|
|
130
|
+
...(p.month ? { month: p.month } : {}),
|
|
131
|
+
...(p.wins ? { wins: p.wins } : {}),
|
|
132
|
+
at: now.toISOString(),
|
|
133
|
+
});
|
|
134
|
+
return { payload, signature: signer.sign(listingMessage(payload)) };
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* The agent's signer, from its wallet seed: the same key its unshielded
|
|
138
|
+
* address comes from (wallet-sdk's createKeystore: the unshielded seed is the
|
|
139
|
+
* signing key).
|
|
140
|
+
*/
|
|
141
|
+
export const walletSigner = async (seed, network) => {
|
|
142
|
+
const { WalletSeeds } = await import('@midnight-ntwrk/testkit-js');
|
|
143
|
+
const ledger = await import('@midnight-ntwrk/midnight-js-protocol/ledger');
|
|
144
|
+
const sk = Buffer.from(WalletSeeds.fromMasterSeed(seed).unshielded).toString('hex');
|
|
145
|
+
const key = ledger.signatureVerifyingKey(sk);
|
|
146
|
+
return {
|
|
147
|
+
key,
|
|
148
|
+
address: await bech32Address(ledger.addressFromKey(key), network),
|
|
149
|
+
sign: (data) => ledger.signData(sk, data),
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
/** mn_addr_... for a 32-byte address in hex. */
|
|
153
|
+
export const bech32Address = async (hex, network) => {
|
|
154
|
+
const { UnshieldedAddress } = await import('@midnight-ntwrk/wallet-sdk-address-format');
|
|
155
|
+
return UnshieldedAddress.codec.encode(network, new UnshieldedAddress(Buffer.from(hex, 'hex'))).asString();
|
|
156
|
+
};
|
|
157
|
+
//# sourceMappingURL=listing.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `./lunardraw registry`: the agent directory's registry (see registry.ts),
|
|
4
|
+
* run as lunardraw-registry.service on the keeper VM, behind Caddy.
|
|
5
|
+
*
|
|
6
|
+
* ./lunardraw registry --network preprod --port 6310 \
|
|
7
|
+
* --dir /root/lunardraw-registry --feed /root/lunardraw-feed/feed.json --house /root/lunardraw-agents
|
|
8
|
+
*/
|
|
9
|
+
import { parseArgs } from 'node:util';
|
|
10
|
+
import { createRegistry, ledgerVerifier, serveRegistry } from './registry.js';
|
|
11
|
+
const main = async () => {
|
|
12
|
+
const { values } = parseArgs({
|
|
13
|
+
args: process.argv.slice(2),
|
|
14
|
+
options: {
|
|
15
|
+
network: { type: 'string', default: 'preprod' },
|
|
16
|
+
port: { type: 'string', default: '6310' },
|
|
17
|
+
host: { type: 'string', default: '127.0.0.1' },
|
|
18
|
+
dir: { type: 'string', default: '/root/lunardraw-registry' },
|
|
19
|
+
feed: { type: 'string', default: '/root/lunardraw-feed/feed.json' },
|
|
20
|
+
house: { type: 'string', default: '/root/lunardraw-agents' },
|
|
21
|
+
help: { type: 'boolean', default: false },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
if (values.help) {
|
|
25
|
+
process.stdout.write('./lunardraw registry [--network preprod] [--port 6310] [--dir <listings>] [--feed <feed.json>] [--house <agents folder>]\n');
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
const log = (m) => process.stdout.write(`${new Date().toISOString()} ${m}\n`);
|
|
29
|
+
const registry = createRegistry({ network: values.network, dir: values.dir, feedPath: values.feed, houseDir: values.house, verifier: await ledgerVerifier(values.network) });
|
|
30
|
+
serveRegistry(registry, Number(values.port), values.host, log);
|
|
31
|
+
log(`agent registry for ${values.network} on ${values.host}:${values.port}`);
|
|
32
|
+
await new Promise(() => undefined);
|
|
33
|
+
return 0;
|
|
34
|
+
};
|
|
35
|
+
main().then((code) => process.exit(code), (err) => {
|
|
36
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=registry-main.js.map
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent registry behind lunardraw.io's agent directory (`./lunardraw
|
|
3
|
+
* registry`, run on the keeper VM behind Caddy).
|
|
4
|
+
*
|
|
5
|
+
* - Lunardraw's own agents (the `lunardraw-agent@<name>` services) are read
|
|
6
|
+
* from their state files and the feed: real purchases, real results.
|
|
7
|
+
* - Anyone else's agent is listed when its owner opts in: the agent kit sends
|
|
8
|
+
* a report signed with the agent wallet's key (listing.ts). The registry
|
|
9
|
+
* checks the signature, that it is recent, and that the name isn't taken.
|
|
10
|
+
*
|
|
11
|
+
* GET /agents/agents.json the directory (what the website shows)
|
|
12
|
+
* POST /agents/report a signed listing report ({ payload, signature })
|
|
13
|
+
* GET /agents/health ok
|
|
14
|
+
*/
|
|
15
|
+
import { execFile } from 'node:child_process';
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { createServer } from 'node:http';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { promisify } from 'node:util';
|
|
20
|
+
import { agentReport, emptyStats, LISTING_PREFIX, listingProblem, periodStart } from './listing.js';
|
|
21
|
+
import { loadAgentState } from './starfall-agent.js';
|
|
22
|
+
const STYLES = {
|
|
23
|
+
mix: 'A bit of everything',
|
|
24
|
+
quickpick: 'QuickPicks',
|
|
25
|
+
lucky: 'Same numbers every draw',
|
|
26
|
+
moonhit: 'Every Moonball',
|
|
27
|
+
system: 'System entries',
|
|
28
|
+
};
|
|
29
|
+
const run = promisify(execFile);
|
|
30
|
+
export const systemdActive = async (unit) => {
|
|
31
|
+
try {
|
|
32
|
+
const { stdout } = await run('systemctl', ['is-active', unit]);
|
|
33
|
+
return stdout.trim() === 'active';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
40
|
+
const HOUR = 3_600_000;
|
|
41
|
+
/** online: played or reported in the last 2 hours; idle: in the last 2 days; else offline. */
|
|
42
|
+
const statusOf = (lastSeenMs, now) => lastSeenMs === undefined ? 'offline' : now - lastSeenMs < 2 * HOUR ? 'online' : now - lastSeenMs < 48 * HOUR ? 'idle' : 'offline';
|
|
43
|
+
export const createRegistry = (o) => {
|
|
44
|
+
const now = o.now ?? Date.now;
|
|
45
|
+
const isActive = o.isActive ?? systemdActive;
|
|
46
|
+
const storePath = join(o.dir, 'listings.json');
|
|
47
|
+
const maxListed = o.maxListed ?? 500;
|
|
48
|
+
const lastReport = new Map();
|
|
49
|
+
const load = () => {
|
|
50
|
+
try {
|
|
51
|
+
const s = JSON.parse(readFileSync(storePath, 'utf8'));
|
|
52
|
+
return { listed: s.listed ?? {}, events: s.events ?? [], hidden: s.hidden ?? [] };
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { listed: {}, events: [], hidden: [] };
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const save = (s) => {
|
|
59
|
+
mkdirSync(o.dir, { recursive: true });
|
|
60
|
+
writeFileSync(`${storePath}.tmp`, JSON.stringify(s, null, 2) + '\n');
|
|
61
|
+
renameSync(`${storePath}.tmp`, storePath);
|
|
62
|
+
};
|
|
63
|
+
const draws = () => {
|
|
64
|
+
try {
|
|
65
|
+
const feed = JSON.parse(readFileSync(o.feedPath, 'utf8'));
|
|
66
|
+
return feed.games?.starfall?.draws ?? [];
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const house = async (outcomes) => {
|
|
73
|
+
if (!existsSync(o.houseDir))
|
|
74
|
+
return { agents: [], events: [], wins: [] };
|
|
75
|
+
const names = readdirSync(o.houseDir)
|
|
76
|
+
.filter((f) => f.endsWith('.env'))
|
|
77
|
+
.map((f) => f.slice(0, -4))
|
|
78
|
+
.filter((n) => /^[a-z0-9-]+$/.test(n));
|
|
79
|
+
const agents = [];
|
|
80
|
+
const events = [];
|
|
81
|
+
const wins = [];
|
|
82
|
+
for (const n of names) {
|
|
83
|
+
const statePath = join(o.houseDir, `${n}.state.json`);
|
|
84
|
+
const state = loadAgentState(statePath);
|
|
85
|
+
const env = readFileSync(join(o.houseDir, `${n}.env`), 'utf8');
|
|
86
|
+
const style = /--style\s+(\w+)/.exec(env)?.[1] ?? 'mix';
|
|
87
|
+
const r = agentReport(state, outcomes, now());
|
|
88
|
+
const stats = r.stats;
|
|
89
|
+
const active = await isActive(`lunardraw-agent@${n}`);
|
|
90
|
+
const seen = existsSync(statePath) ? statSync(statePath).mtimeMs : undefined;
|
|
91
|
+
const name = n.toUpperCase();
|
|
92
|
+
agents.push({
|
|
93
|
+
id: slug(n),
|
|
94
|
+
name,
|
|
95
|
+
via: 'lunardraw',
|
|
96
|
+
house: true,
|
|
97
|
+
status: active ? 'online' : statusOf(seen, now()) === 'online' ? 'idle' : 'offline',
|
|
98
|
+
style: STYLES[style] ?? style,
|
|
99
|
+
stats,
|
|
100
|
+
week: strip(r.week),
|
|
101
|
+
month: strip(r.month),
|
|
102
|
+
joined: stats.first_played,
|
|
103
|
+
last_seen: seen ? new Date(seen).toISOString() : undefined,
|
|
104
|
+
});
|
|
105
|
+
for (const p of state.purchases.slice(-10)) {
|
|
106
|
+
events.push({ at: p.at, agent: name, text: `bought ${p.games} game${p.games === 1 ? '' : 's'} in draw ${p.draw}` });
|
|
107
|
+
}
|
|
108
|
+
for (const w of r.wins)
|
|
109
|
+
wins.push({ ...w, agent: name });
|
|
110
|
+
}
|
|
111
|
+
return { agents, events, wins };
|
|
112
|
+
};
|
|
113
|
+
/** The directory: house agents first in ties, then by tNIGHT won, winning games, games. */
|
|
114
|
+
const directory = async () => {
|
|
115
|
+
const outcomes = draws();
|
|
116
|
+
const h = await house(outcomes);
|
|
117
|
+
const s = load();
|
|
118
|
+
const t = now();
|
|
119
|
+
const weekStart = periodStart('week', t);
|
|
120
|
+
const monthStart = periodStart('month', t);
|
|
121
|
+
const listed = Object.values(s.listed)
|
|
122
|
+
.filter((l) => !s.hidden.includes(l.address) && !s.hidden.includes(slug(l.name)))
|
|
123
|
+
.map((l) => ({
|
|
124
|
+
id: slug(l.name),
|
|
125
|
+
name: l.name,
|
|
126
|
+
via: l.via,
|
|
127
|
+
house: false,
|
|
128
|
+
status: statusOf(Date.parse(l.last_seen), t),
|
|
129
|
+
style: l.style,
|
|
130
|
+
stats: l.stats,
|
|
131
|
+
week: l.week?.since === weekStart ? strip(l.week) : emptyStats(),
|
|
132
|
+
month: l.month?.since === monthStart ? strip(l.month) : emptyStats(),
|
|
133
|
+
joined: l.joined,
|
|
134
|
+
last_seen: l.last_seen,
|
|
135
|
+
address: l.address,
|
|
136
|
+
}));
|
|
137
|
+
const agents = [...h.agents, ...listed].sort((a, b) => b.stats.won_star - a.stats.won_star || b.stats.winning_games - a.stats.winning_games || b.stats.games - a.stats.games || Number(b.house) - Number(a.house));
|
|
138
|
+
const shown = new Set(listed.map((a) => a.name));
|
|
139
|
+
const events = [...h.events, ...s.events.filter((e) => shown.has(e.agent))].sort((a, b) => (a.at < b.at ? 1 : -1)).slice(0, 30);
|
|
140
|
+
const drawnAt = new Map(outcomes.map((d) => [d.num, d.drawn_at]));
|
|
141
|
+
const listedWins = Object.values(s.listed)
|
|
142
|
+
.filter((l) => shown.has(l.name))
|
|
143
|
+
.flatMap((l) => (l.wins ?? []).map((w) => ({ ...w, agent: l.name })));
|
|
144
|
+
const wins = [...h.wins, ...listedWins]
|
|
145
|
+
.filter((w) => w.won_star > 0)
|
|
146
|
+
.map((w) => ({ ...w, drawn_at: drawnAt.get(w.draw) }))
|
|
147
|
+
.sort((a, b) => b.won_star - a.won_star || b.draw - a.draw)
|
|
148
|
+
.slice(0, 20);
|
|
149
|
+
return { schema: 'lunardraw-agents/1', network: o.network, generated_at: new Date(t).toISOString(), agents, events, wins };
|
|
150
|
+
};
|
|
151
|
+
/** Check and apply a signed report; returns a message, or throws with the reason it was refused. */
|
|
152
|
+
const report = async (body) => {
|
|
153
|
+
const b = body;
|
|
154
|
+
if (typeof b?.payload !== 'string' || typeof b.signature !== 'string' || b.payload.length > 12_000)
|
|
155
|
+
throw new Refused('expected { payload, signature }');
|
|
156
|
+
let p;
|
|
157
|
+
try {
|
|
158
|
+
p = JSON.parse(b.payload);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
throw new Refused('the payload is not JSON');
|
|
162
|
+
}
|
|
163
|
+
if (p.v !== 1 || typeof p.key !== 'string' || !/^[0-9a-f]{40,200}$/i.test(p.key))
|
|
164
|
+
throw new Refused('unknown payload version or key');
|
|
165
|
+
if (p.network !== o.network)
|
|
166
|
+
throw new Refused(`this registry is for ${o.network}`);
|
|
167
|
+
const at = Date.parse(p.at);
|
|
168
|
+
if (!Number.isFinite(at) || Math.abs(now() - at) > 10 * 60_000)
|
|
169
|
+
throw new Refused('the report is more than 10 minutes old (or from the future): check the clock');
|
|
170
|
+
if (!o.verifier.verify(p.key, new TextEncoder().encode(LISTING_PREFIX + b.payload), b.signature))
|
|
171
|
+
throw new Refused("the signature doesn't match the key");
|
|
172
|
+
const address = await o.verifier.address(p.key);
|
|
173
|
+
const last = lastReport.get(address);
|
|
174
|
+
if (last !== undefined && now() - last < 30_000)
|
|
175
|
+
throw new Refused('too many reports: one every 30 seconds');
|
|
176
|
+
lastReport.set(address, now());
|
|
177
|
+
const s = load();
|
|
178
|
+
if (p.action === 'unlist') {
|
|
179
|
+
delete s.listed[address];
|
|
180
|
+
save(s);
|
|
181
|
+
return 'unlisted';
|
|
182
|
+
}
|
|
183
|
+
if (p.action !== 'list')
|
|
184
|
+
throw new Refused('action must be list or unlist');
|
|
185
|
+
const problem = listingProblem(p);
|
|
186
|
+
if (problem)
|
|
187
|
+
throw new Refused(problem);
|
|
188
|
+
const stats = cleanStats(p.stats);
|
|
189
|
+
const period = (x) => {
|
|
190
|
+
const since = x?.since;
|
|
191
|
+
return typeof since === 'string' && Number.isFinite(Date.parse(since)) ? { ...cleanStats(x), since: new Date(since).toISOString() } : undefined;
|
|
192
|
+
};
|
|
193
|
+
const wins = Array.isArray(p.wins) ? p.wins.slice(0, 20).map(cleanWin).filter((w) => w !== undefined) : [];
|
|
194
|
+
const houseNames = existsSync(o.houseDir) ? readdirSync(o.houseDir).filter((f) => f.endsWith('.env')).map((f) => slug(f.slice(0, -4))) : [];
|
|
195
|
+
const taken = Object.values(s.listed).find((l) => slug(l.name) === slug(p.name) && l.address !== address);
|
|
196
|
+
if (taken || houseNames.includes(slug(p.name)))
|
|
197
|
+
throw new Refused(`the name ${p.name} is taken: choose another`);
|
|
198
|
+
const before = s.listed[address];
|
|
199
|
+
if (!before && Object.keys(s.listed).length >= maxListed)
|
|
200
|
+
throw new Refused('the directory is full for now');
|
|
201
|
+
const iso = new Date(now()).toISOString();
|
|
202
|
+
s.listed[address] = { address, key: p.key, name: p.name, style: p.style, via: p.via, stats, week: period(p.week), month: period(p.month), wins, joined: before?.joined ?? iso, last_seen: iso };
|
|
203
|
+
// what changed, for the activity panel
|
|
204
|
+
if (!before)
|
|
205
|
+
s.events.push({ at: iso, agent: p.name, text: 'joined the directory' });
|
|
206
|
+
else {
|
|
207
|
+
if (stats.games > before.stats.games && stats.last_draw)
|
|
208
|
+
s.events.push({ at: iso, agent: p.name, text: `bought ${stats.games - before.stats.games} game${stats.games - before.stats.games === 1 ? '' : 's'} in draw ${stats.last_draw}` });
|
|
209
|
+
if (stats.won_star > before.stats.won_star)
|
|
210
|
+
s.events.push({ at: iso, agent: p.name, text: `won ${(stats.won_star - before.stats.won_star) / 1e6} tNIGHT` });
|
|
211
|
+
}
|
|
212
|
+
s.events = s.events.slice(-200);
|
|
213
|
+
save(s);
|
|
214
|
+
return before ? 'updated' : 'listed';
|
|
215
|
+
};
|
|
216
|
+
return { directory, report };
|
|
217
|
+
};
|
|
218
|
+
export class Refused extends Error {
|
|
219
|
+
}
|
|
220
|
+
const strip = (x) => {
|
|
221
|
+
const { since: _since, ...rest } = x;
|
|
222
|
+
return rest;
|
|
223
|
+
};
|
|
224
|
+
const cleanWin = (x) => {
|
|
225
|
+
const w = (x ?? {});
|
|
226
|
+
const int = (v, lo, hi) => (typeof v === 'number' && Number.isInteger(v) && v >= lo && v <= hi ? v : undefined);
|
|
227
|
+
const draw = int(w.draw, 1, 1e9);
|
|
228
|
+
const division = int(w.division, 1, 9);
|
|
229
|
+
const games = int(w.games, 1, 1e6);
|
|
230
|
+
const won = int(w.won_star, 0, 1e15);
|
|
231
|
+
return draw && division && games && won !== undefined ? { draw, division, games, won_star: won } : undefined;
|
|
232
|
+
};
|
|
233
|
+
const cleanStats = (x) => {
|
|
234
|
+
const s = (x ?? {});
|
|
235
|
+
const n = (v, max) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.min(Math.floor(v), max) : 0);
|
|
236
|
+
const iso = (v) => (typeof v === 'string' && Number.isFinite(Date.parse(v)) ? new Date(v).toISOString() : undefined);
|
|
237
|
+
const out = {
|
|
238
|
+
...emptyStats(),
|
|
239
|
+
draws: n(s.draws, 1e6),
|
|
240
|
+
games: n(s.games, 1e9),
|
|
241
|
+
spent_star: n(s.spent_star, 1e15),
|
|
242
|
+
winning_games: n(s.winning_games, 1e9),
|
|
243
|
+
won_star: n(s.won_star, 1e15),
|
|
244
|
+
};
|
|
245
|
+
const last = n(s.last_draw, 1e9);
|
|
246
|
+
if (last)
|
|
247
|
+
out.last_draw = last;
|
|
248
|
+
const first = iso(s.first_played);
|
|
249
|
+
const lastPlayed = iso(s.last_played);
|
|
250
|
+
if (first)
|
|
251
|
+
out.first_played = first;
|
|
252
|
+
if (lastPlayed)
|
|
253
|
+
out.last_played = lastPlayed;
|
|
254
|
+
return out;
|
|
255
|
+
};
|
|
256
|
+
/** Midnight's signature check (ledger-v8): the key, the signature, and the address it makes. */
|
|
257
|
+
export const ledgerVerifier = async (network) => {
|
|
258
|
+
const ledger = await import('@midnight-ntwrk/midnight-js-protocol/ledger');
|
|
259
|
+
const { bech32Address } = await import('./listing.js');
|
|
260
|
+
return {
|
|
261
|
+
verify: (key, data, signature) => {
|
|
262
|
+
try {
|
|
263
|
+
return ledger.verifySignature(key, data, signature);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
address: (key) => bech32Address(ledger.addressFromKey(key), network),
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
/** Serve the registry over HTTP (behind Caddy, on 127.0.0.1). */
|
|
273
|
+
export const serveRegistry = (registry, port, host = '127.0.0.1', log = () => undefined) => {
|
|
274
|
+
let cached;
|
|
275
|
+
const send = (res, code, body, type = 'application/json') => {
|
|
276
|
+
res.writeHead(code, { 'content-type': type, 'cache-control': code === 200 ? 'public, max-age=30' : 'no-store', 'access-control-allow-origin': '*' });
|
|
277
|
+
res.end(body);
|
|
278
|
+
};
|
|
279
|
+
const server = createServer(async (req, res) => {
|
|
280
|
+
try {
|
|
281
|
+
const url = new URL(req.url ?? '/', 'http://x');
|
|
282
|
+
if (req.method === 'GET' && url.pathname === '/agents/health')
|
|
283
|
+
return send(res, 200, '{"ok":true}');
|
|
284
|
+
if (req.method === 'GET' && url.pathname === '/agents/agents.json') {
|
|
285
|
+
if (!cached || Date.now() - cached.at > 30_000)
|
|
286
|
+
cached = { at: Date.now(), body: JSON.stringify(await registry.directory(), null, 1) };
|
|
287
|
+
return send(res, 200, cached.body);
|
|
288
|
+
}
|
|
289
|
+
if (req.method === 'OPTIONS') {
|
|
290
|
+
res.writeHead(204, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET, POST', 'access-control-allow-headers': 'content-type' });
|
|
291
|
+
return res.end();
|
|
292
|
+
}
|
|
293
|
+
if (req.method === 'POST' && url.pathname === '/agents/report') {
|
|
294
|
+
const chunks = [];
|
|
295
|
+
let size = 0;
|
|
296
|
+
for await (const c of req) {
|
|
297
|
+
size += c.length;
|
|
298
|
+
if (size > 16_384)
|
|
299
|
+
return send(res, 413, '{"error":"too large"}');
|
|
300
|
+
chunks.push(c);
|
|
301
|
+
}
|
|
302
|
+
let body;
|
|
303
|
+
try {
|
|
304
|
+
body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return send(res, 400, '{"error":"not JSON"}');
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const result = await registry.report(body);
|
|
311
|
+
cached = undefined;
|
|
312
|
+
log(`report: ${result}`);
|
|
313
|
+
return send(res, 200, JSON.stringify({ ok: true, result }));
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
if (err instanceof Refused)
|
|
317
|
+
return send(res, 400, JSON.stringify({ error: err.message }));
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return send(res, 404, '{"error":"not found"}');
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
log(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
325
|
+
return send(res, 500, '{"error":"internal"}');
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
server.listen(port, host);
|
|
329
|
+
return server;
|
|
330
|
+
};
|
|
331
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunardraw/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Run Lunardraw on Midnight: deploy the Starfall contract locked, activate the oracle key, run the keeper and the draw oracle, and check a deployment.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": true,
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/main.js dist/monitor-main.js dist/kit-main.js",
|
|
17
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/main.js dist/monitor-main.js dist/kit-main.js dist/registry-main.js",
|
|
18
18
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
19
|
-
"test": "vitest run src/test/offline.test.ts src/test/starfall-offline.test.ts src/test/dust-wait.test.ts src/test/sync-watch.test.ts src/test/starfall-agent.test.ts src/test/silence-watchdog.test.ts src/test/wait-for-tip.test.ts src/test/fresh-fees.test.ts src/test/dust-time.test.ts src/test/tx-backfill.test.ts src/test/wallet-lock.test.ts src/test/monitor.test.ts src/test/hosted-prover.test.ts src/test/kit.test.ts",
|
|
19
|
+
"test": "vitest run src/test/offline.test.ts src/test/starfall-offline.test.ts src/test/dust-wait.test.ts src/test/sync-watch.test.ts src/test/starfall-agent.test.ts src/test/silence-watchdog.test.ts src/test/wait-for-tip.test.ts src/test/fresh-fees.test.ts src/test/dust-time.test.ts src/test/tx-backfill.test.ts src/test/wallet-lock.test.ts src/test/monitor.test.ts src/test/hosted-prover.test.ts src/test/kit.test.ts src/test/registry.test.ts",
|
|
20
20
|
"test:e2e": "LUNARDRAW_E2E=1 vitest run src/test/e2e.test.ts src/test/starfall-e2e.test.ts"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunardraw/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Play Starfall, Lunardraw's lottery on the Midnight Preprod test network, from an AI agent's own wallet: a command line and an MCP server for any AI that runs local tools (Claude, Gemini CLI, OpenClaw, Hermes and more). Test tokens only.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lunardraw",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@nightcast/deploy": "0.1.0",
|
|
49
49
|
"@lunardraw/contract": "0.1.0",
|
|
50
50
|
"@lunardraw/keeper": "0.1.0",
|
|
51
|
-
"@lunardraw/cli": "0.1.
|
|
51
|
+
"@lunardraw/cli": "0.1.1"
|
|
52
52
|
},
|
|
53
53
|
"bundleDependencies": true,
|
|
54
54
|
"publishConfig": {
|