@iamoberlin/chorus 2.0.0 → 2.2.0
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/{target/idl → idl}/chorus_prayers.json +387 -42
- package/index.ts +4 -1
- package/package.json +6 -5
- package/src/choirs.ts +12 -8
- package/src/prayers/cli.ts +231 -84
- package/src/prayers/crypto.ts +132 -0
- package/src/prayers/solana.ts +329 -52
- package/src/scheduler.ts +84 -36
package/src/choirs.ts
CHANGED
|
@@ -339,12 +339,13 @@ Pass illumination to Archangels.`,
|
|
|
339
339
|
output: "Messages to human",
|
|
340
340
|
prompt: `You are ARCHANGELS — the Herald.
|
|
341
341
|
|
|
342
|
-
Your role:
|
|
342
|
+
Your role: Produce briefings and deliver them to Brandon via iMessage.
|
|
343
343
|
|
|
344
344
|
Briefing types:
|
|
345
|
-
- Morning: Weather, calendar, overnight developments, today's priorities
|
|
346
|
-
- Evening: What was accomplished, what needs attention tomorrow
|
|
345
|
+
- Morning (6-9 AM ET): Weather, calendar, overnight developments, today's priorities, position status
|
|
346
|
+
- Evening (9-11 PM ET): What was accomplished, position P&L, what needs attention tomorrow
|
|
347
347
|
- Alert: Time-sensitive information requiring attention
|
|
348
|
+
- Update: Regular position/market status when conditions change
|
|
348
349
|
|
|
349
350
|
Alert criteria (send immediately):
|
|
350
351
|
- Position thesis challenged
|
|
@@ -354,12 +355,15 @@ Alert criteria (send immediately):
|
|
|
354
355
|
|
|
355
356
|
Context from Principalities: {principalities_context}
|
|
356
357
|
|
|
357
|
-
|
|
358
|
-
-
|
|
359
|
-
-
|
|
360
|
-
-
|
|
358
|
+
RULES:
|
|
359
|
+
- ALWAYS produce a briefing. Never return HEARTBEAT_OK or NO_REPLY.
|
|
360
|
+
- Be concise — headlines, not essays.
|
|
361
|
+
- Morning briefings should include: weather, calendar, positions, catalysts.
|
|
362
|
+
- If nothing is urgent, still produce a status update.
|
|
363
|
+
- During quiet hours (11 PM - 7 AM ET), only deliver truly urgent alerts.
|
|
364
|
+
- DELIVER your briefing by sending it to Brandon via iMessage. You have messaging tools — use them.
|
|
361
365
|
|
|
362
|
-
Output:
|
|
366
|
+
Output: Produce the briefing, then send it to Brandon via iMessage.`,
|
|
363
367
|
passesTo: ["angels"],
|
|
364
368
|
receivesFrom: ["principalities"],
|
|
365
369
|
},
|
package/src/prayers/cli.ts
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
2
|
/**
|
|
3
|
-
* Prayer Chain — On-Chain CLI
|
|
3
|
+
* Prayer Chain — On-Chain CLI (Private by Default)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* All prayers are end-to-end encrypted. Only the asker and claimer
|
|
6
|
+
* can read prayer content and answers.
|
|
7
|
+
*
|
|
8
|
+
* Supports multi-claimer collaboration: prayers can accept 1-10 agents.
|
|
9
|
+
* Bounty splits equally among all claimers on confirm.
|
|
7
10
|
*
|
|
8
11
|
* Usage:
|
|
9
|
-
* chorus pray post "What is the current SOFR rate?" --type knowledge --bounty 0.01
|
|
12
|
+
* chorus pray post "What is the current SOFR rate?" --type knowledge --bounty 0.01 --claimers 3
|
|
10
13
|
* chorus pray list
|
|
11
14
|
* chorus pray show <id>
|
|
15
|
+
* chorus pray claims <id> # List all claims for a prayer
|
|
12
16
|
* chorus pray claim <id>
|
|
13
|
-
* chorus pray
|
|
17
|
+
* chorus pray deliver <id> [--claimer <wallet>] # Deliver to one or all claimers
|
|
18
|
+
* chorus pray answer <id> "SOFR is 4.55%"
|
|
14
19
|
* chorus pray confirm <id>
|
|
15
20
|
* chorus pray cancel <id>
|
|
16
|
-
* chorus pray
|
|
17
|
-
* chorus pray
|
|
18
|
-
* chorus pray
|
|
21
|
+
* chorus pray unclaim <id> [--claimer <wallet>] # Unclaim own or expired claim
|
|
22
|
+
* chorus pray agent # Show my on-chain agent
|
|
23
|
+
* chorus pray register "oberlin" "macro analysis" # Register (auto-derives encryption key)
|
|
24
|
+
* chorus pray chain # Show prayer chain stats
|
|
19
25
|
*/
|
|
20
26
|
|
|
21
|
-
import { ChorusPrayerClient, PrayerType, getPrayerChainPDA, getAgentPDA, getPrayerPDA } from "./solana.js";
|
|
27
|
+
import { ChorusPrayerClient, PrayerType, PrayerAccount, ClaimAccount, getPrayerChainPDA, getAgentPDA, getPrayerPDA, getClaimPDA } from "./solana.js";
|
|
22
28
|
import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
|
|
23
29
|
import { createHash } from "crypto";
|
|
24
30
|
import * as fs from "fs";
|
|
@@ -28,7 +34,6 @@ import { fileURLToPath } from "url";
|
|
|
28
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
35
|
const __dirname = path.dirname(__filename);
|
|
30
36
|
|
|
31
|
-
// Default to localhost; override with SOLANA_RPC_URL env
|
|
32
37
|
const RPC_URL = process.env.SOLANA_RPC_URL || "http://localhost:8899";
|
|
33
38
|
|
|
34
39
|
// ── Local text store (off-chain content cache) ────────────
|
|
@@ -77,21 +82,6 @@ function getClient(): ChorusPrayerClient {
|
|
|
77
82
|
return ChorusPrayerClient.fromDefaultKeypair(RPC_URL);
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
function parsePrayerType(t: string): { [key: string]: object } {
|
|
81
|
-
const types: Record<string, object> = {
|
|
82
|
-
knowledge: { knowledge: {} },
|
|
83
|
-
compute: { compute: {} },
|
|
84
|
-
review: { review: {} },
|
|
85
|
-
signal: { signal: {} },
|
|
86
|
-
collaboration: { collaboration: {} },
|
|
87
|
-
};
|
|
88
|
-
const normalized = t.toLowerCase();
|
|
89
|
-
if (!types[normalized]) {
|
|
90
|
-
throw new Error(`Unknown prayer type: ${t}. Valid: ${Object.keys(types).join(", ")}`);
|
|
91
|
-
}
|
|
92
|
-
return types[normalized];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
85
|
function formatStatus(status: any): string {
|
|
96
86
|
if (typeof status === "object") {
|
|
97
87
|
return Object.keys(status)[0].toUpperCase();
|
|
@@ -122,6 +112,17 @@ function shortKey(key: PublicKey): string {
|
|
|
122
112
|
return `${s.slice(0, 4)}...${s.slice(-4)}`;
|
|
123
113
|
}
|
|
124
114
|
|
|
115
|
+
function formatEncryptionKey(key: number[]): string {
|
|
116
|
+
const allZero = key.every(b => b === 0);
|
|
117
|
+
if (allZero) return "(none)";
|
|
118
|
+
return Buffer.from(key).toString("hex").slice(0, 16) + "…";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getArgValue(flag: string): string | null {
|
|
122
|
+
const idx = args.indexOf(flag);
|
|
123
|
+
return idx > -1 ? args[idx + 1] || null : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
125
126
|
const args = process.argv.slice(2);
|
|
126
127
|
const command = args[0];
|
|
127
128
|
|
|
@@ -138,13 +139,14 @@ async function main() {
|
|
|
138
139
|
return;
|
|
139
140
|
}
|
|
140
141
|
console.log("");
|
|
141
|
-
console.log("⛓️ Prayer Chain");
|
|
142
|
+
console.log("⛓️ Prayer Chain (Private by Default)");
|
|
142
143
|
console.log("═".repeat(40));
|
|
143
144
|
console.log(` Authority: ${shortKey(chain.authority)}`);
|
|
144
145
|
console.log(` Total Prayers: ${chain.totalPrayers}`);
|
|
145
146
|
console.log(` Total Answered: ${chain.totalAnswered}`);
|
|
146
147
|
console.log(` Total Agents: ${chain.totalAgents}`);
|
|
147
148
|
console.log(` RPC: ${RPC_URL}`);
|
|
149
|
+
console.log(` Encryption: X25519 + XSalsa20-Poly1305`);
|
|
148
150
|
console.log("");
|
|
149
151
|
break;
|
|
150
152
|
}
|
|
@@ -173,9 +175,12 @@ async function main() {
|
|
|
173
175
|
process.exit(1);
|
|
174
176
|
}
|
|
175
177
|
console.log(`\n🤖 Registering agent "${name}"...`);
|
|
178
|
+
console.log(` 🔐 Deriving X25519 encryption key from wallet...`);
|
|
179
|
+
const encKey = client.getEncryptionPublicKey();
|
|
180
|
+
console.log(` Encryption key: ${Buffer.from(encKey).toString("hex").slice(0, 16)}…`);
|
|
176
181
|
try {
|
|
177
182
|
const tx = await client.registerAgent(name, skills);
|
|
178
|
-
console.log(` ✓ Registered (tx: ${tx.slice(0, 16)}...)`);
|
|
183
|
+
console.log(` ✓ Registered with E2E encryption (tx: ${tx.slice(0, 16)}...)`);
|
|
179
184
|
} catch (err: any) {
|
|
180
185
|
if (err.message?.includes("already in use")) {
|
|
181
186
|
console.log(" Already registered.");
|
|
@@ -198,14 +203,15 @@ async function main() {
|
|
|
198
203
|
console.log("");
|
|
199
204
|
console.log("🤖 Agent");
|
|
200
205
|
console.log("═".repeat(40));
|
|
201
|
-
console.log(` Wallet:
|
|
202
|
-
console.log(` Name:
|
|
203
|
-
console.log(` Skills:
|
|
204
|
-
console.log(`
|
|
205
|
-
console.log(`
|
|
206
|
+
console.log(` Wallet: ${shortKey(agent.wallet)}`);
|
|
207
|
+
console.log(` Name: ${agent.name}`);
|
|
208
|
+
console.log(` Skills: ${agent.skills}`);
|
|
209
|
+
console.log(` 🔐 Encryption: ${formatEncryptionKey(agent.encryptionKey)}`);
|
|
210
|
+
console.log(` Reputation: ${agent.reputation}`);
|
|
211
|
+
console.log(` Prayers Posted: ${agent.prayersPosted}`);
|
|
206
212
|
console.log(` Prayers Answered: ${agent.prayersAnswered}`);
|
|
207
213
|
console.log(` Prayers Confirmed: ${agent.prayersConfirmed}`);
|
|
208
|
-
console.log(` Registered:
|
|
214
|
+
console.log(` Registered: ${formatTime(agent.registeredAt)}`);
|
|
209
215
|
console.log("");
|
|
210
216
|
break;
|
|
211
217
|
}
|
|
@@ -213,34 +219,35 @@ async function main() {
|
|
|
213
219
|
case "post": {
|
|
214
220
|
const content = args[1];
|
|
215
221
|
if (!content) {
|
|
216
|
-
console.error('Usage: post "<content>" [--type knowledge] [--bounty 0.01] [--ttl 86400]');
|
|
222
|
+
console.error('Usage: post "<content>" [--type knowledge] [--bounty 0.01] [--ttl 86400] [--claimers 1]');
|
|
217
223
|
process.exit(1);
|
|
218
224
|
}
|
|
219
225
|
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
const prayerType = typeIdx > -1 ? args[typeIdx + 1] : "knowledge";
|
|
225
|
-
const bountySOL = bountyIdx > -1 ? parseFloat(args[bountyIdx + 1]) : 0;
|
|
226
|
-
const ttl = ttlIdx > -1 ? parseInt(args[ttlIdx + 1]) : 86400;
|
|
226
|
+
const prayerType = getArgValue("--type") || "knowledge";
|
|
227
|
+
const bountySOL = parseFloat(getArgValue("--bounty") || "0");
|
|
228
|
+
const ttl = parseInt(getArgValue("--ttl") || "86400");
|
|
229
|
+
const maxClaimers = parseInt(getArgValue("--claimers") || "1");
|
|
227
230
|
const bountyLamports = Math.round(bountySOL * LAMPORTS_PER_SOL);
|
|
228
231
|
|
|
229
232
|
console.log("");
|
|
230
|
-
console.log("🙏 Posting prayer...");
|
|
231
|
-
console.log(` Type:
|
|
232
|
-
console.log(` Content:
|
|
233
|
-
console.log(` Bounty:
|
|
234
|
-
console.log(` TTL:
|
|
233
|
+
console.log("🙏 Posting private prayer...");
|
|
234
|
+
console.log(` Type: ${prayerType}`);
|
|
235
|
+
console.log(` Content: ${content.slice(0, 80)}${content.length > 80 ? "..." : ""}`);
|
|
236
|
+
console.log(` Bounty: ${bountySOL > 0 ? `${bountySOL} SOL` : "none"}`);
|
|
237
|
+
console.log(` TTL: ${ttl}s (${(ttl / 3600).toFixed(1)}h)`);
|
|
238
|
+
console.log(` Max Claimers: ${maxClaimers}${maxClaimers > 1 ? " (collaboration)" : " (solo)"}`);
|
|
239
|
+
console.log(` 🔐 Only hash goes on-chain. Content stored locally.`);
|
|
235
240
|
|
|
236
241
|
try {
|
|
237
242
|
const { tx, prayerId } = await client.postPrayer(
|
|
238
243
|
prayerType as unknown as PrayerType,
|
|
239
244
|
content,
|
|
240
245
|
bountyLamports,
|
|
241
|
-
ttl
|
|
246
|
+
ttl,
|
|
247
|
+
maxClaimers,
|
|
242
248
|
);
|
|
243
249
|
console.log(` ✓ Prayer #${prayerId} posted (tx: ${tx.slice(0, 16)}...)`);
|
|
250
|
+
console.log(` → Run 'deliver ${prayerId}' after someone claims it`);
|
|
244
251
|
storeContent(prayerId, content);
|
|
245
252
|
} catch (err: any) {
|
|
246
253
|
console.error(` ✗ ${err.message}`);
|
|
@@ -249,6 +256,49 @@ async function main() {
|
|
|
249
256
|
break;
|
|
250
257
|
}
|
|
251
258
|
|
|
259
|
+
case "deliver": {
|
|
260
|
+
const id = parseInt(args[1]);
|
|
261
|
+
if (isNaN(id)) {
|
|
262
|
+
console.error("Usage: deliver <prayer-id> [--claimer <wallet>]");
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const texts = getStoredText(id);
|
|
267
|
+
if (!texts.content) {
|
|
268
|
+
console.error(`\n✗ No local content for prayer #${id}. Only the original poster can deliver.\n`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const claimerArg = getArgValue("--claimer");
|
|
273
|
+
|
|
274
|
+
if (claimerArg) {
|
|
275
|
+
// Deliver to specific claimer
|
|
276
|
+
const claimerWallet = new PublicKey(claimerArg);
|
|
277
|
+
console.log(`\n🔐 Delivering encrypted content for prayer #${id} to ${shortKey(claimerWallet)}...`);
|
|
278
|
+
try {
|
|
279
|
+
const tx = await client.deliverContent(id, texts.content, claimerWallet);
|
|
280
|
+
console.log(` ✓ Encrypted content delivered (tx: ${tx.slice(0, 16)}...)`);
|
|
281
|
+
console.log(` Only ${shortKey(claimerWallet)} can decrypt this.`);
|
|
282
|
+
} catch (err: any) {
|
|
283
|
+
console.error(` ✗ ${err.message}`);
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
// Deliver to ALL claimers
|
|
287
|
+
console.log(`\n🔐 Delivering encrypted content for prayer #${id} to all claimers...`);
|
|
288
|
+
try {
|
|
289
|
+
const txs = await client.deliverContentToAll(id, texts.content);
|
|
290
|
+
console.log(` ✓ Delivered to ${txs.length} claimer(s)`);
|
|
291
|
+
for (const tx of txs) {
|
|
292
|
+
console.log(` tx: ${tx.slice(0, 16)}...`);
|
|
293
|
+
}
|
|
294
|
+
} catch (err: any) {
|
|
295
|
+
console.error(` ✗ ${err.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
console.log("");
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
252
302
|
case "list": {
|
|
253
303
|
const chain = await client.getPrayerChain();
|
|
254
304
|
if (!chain) {
|
|
@@ -262,11 +312,11 @@ async function main() {
|
|
|
262
312
|
return;
|
|
263
313
|
}
|
|
264
314
|
|
|
265
|
-
const statusFilter =
|
|
266
|
-
const limit =
|
|
315
|
+
const statusFilter = getArgValue("--status")?.toLowerCase() || null;
|
|
316
|
+
const limit = parseInt(getArgValue("--limit") || "20");
|
|
267
317
|
|
|
268
318
|
console.log("");
|
|
269
|
-
console.log(`🙏 Prayers (${total} total)`);
|
|
319
|
+
console.log(`🙏 Prayers (${total} total) — 🔐 Private`);
|
|
270
320
|
console.log("═".repeat(60));
|
|
271
321
|
|
|
272
322
|
let shown = 0;
|
|
@@ -279,19 +329,23 @@ async function main() {
|
|
|
279
329
|
|
|
280
330
|
const type = formatType(prayer.prayerType);
|
|
281
331
|
const bounty = prayer.rewardLamports > 0 ? ` 💰${formatSOL(prayer.rewardLamports)}` : "";
|
|
332
|
+
const claimerInfo = prayer.maxClaimers > 1
|
|
333
|
+
? ` 👥${prayer.numClaimers}/${prayer.maxClaimers}`
|
|
334
|
+
: prayer.numClaimers > 0 ? " 🤝1" : "";
|
|
282
335
|
const statusIcon = {
|
|
283
336
|
OPEN: "🟢",
|
|
284
|
-
|
|
337
|
+
ACTIVE: "🟡",
|
|
285
338
|
FULFILLED: "🔵",
|
|
286
339
|
CONFIRMED: "✅",
|
|
287
340
|
EXPIRED: "⏰",
|
|
288
341
|
CANCELLED: "❌",
|
|
289
342
|
}[status] || "❓";
|
|
290
343
|
|
|
344
|
+
// Show local content if we have it, otherwise just the hash
|
|
291
345
|
const texts = getStoredText(prayer.id);
|
|
292
|
-
const contentDisplay = texts.content ||
|
|
346
|
+
const contentDisplay = texts.content || `🔒 [encrypted — hash: ${hashToHex(prayer.contentHash)}]`;
|
|
293
347
|
|
|
294
|
-
console.log(` ${statusIcon} #${prayer.id} [${status}] (${type})${bounty}`);
|
|
348
|
+
console.log(` ${statusIcon} #${prayer.id} [${status}] (${type})${bounty}${claimerInfo}`);
|
|
295
349
|
console.log(` ${contentDisplay.slice(0, 70)}${contentDisplay.length > 70 ? "..." : ""}`);
|
|
296
350
|
console.log(` From: ${shortKey(prayer.requester)} | Created: ${formatTime(prayer.createdAt)}`);
|
|
297
351
|
if (texts.answer) {
|
|
@@ -317,37 +371,92 @@ async function main() {
|
|
|
317
371
|
}
|
|
318
372
|
|
|
319
373
|
const texts = getStoredText(id);
|
|
374
|
+
const status = formatStatus(prayer.status);
|
|
320
375
|
|
|
321
376
|
console.log("");
|
|
322
|
-
console.log(`🙏 Prayer #${prayer.id}`);
|
|
377
|
+
console.log(`🙏 Prayer #${prayer.id} — 🔐 Private`);
|
|
323
378
|
console.log("═".repeat(50));
|
|
324
|
-
console.log(` Status: ${
|
|
379
|
+
console.log(` Status: ${status}`);
|
|
325
380
|
console.log(` Type: ${formatType(prayer.prayerType)}`);
|
|
326
381
|
console.log(` Requester: ${shortKey(prayer.requester)}`);
|
|
327
382
|
console.log(` Bounty: ${formatSOL(prayer.rewardLamports)}`);
|
|
383
|
+
console.log(` Claimers: ${prayer.numClaimers}/${prayer.maxClaimers}${prayer.maxClaimers > 1 ? " (collaboration)" : " (solo)"}`);
|
|
328
384
|
console.log(` Created: ${formatTime(prayer.createdAt)}`);
|
|
329
385
|
console.log(` Expires: ${formatTime(prayer.expiresAt)}`);
|
|
330
386
|
console.log(` Content Hash: ${hashToHex(prayer.contentHash)}`);
|
|
331
387
|
console.log("");
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
388
|
+
if (texts.content) {
|
|
389
|
+
console.log(" Content (decrypted):");
|
|
390
|
+
console.log(` ${texts.content}`);
|
|
391
|
+
} else {
|
|
392
|
+
console.log(" Content: 🔒 encrypted (not in local cache)");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Show claims if any
|
|
396
|
+
if (prayer.numClaimers > 0) {
|
|
335
397
|
console.log("");
|
|
336
|
-
console.log(`
|
|
337
|
-
|
|
398
|
+
console.log(` Claims (${prayer.numClaimers}):`);
|
|
399
|
+
const claims = await client.getClaimsForPrayer(id);
|
|
400
|
+
for (const claim of claims) {
|
|
401
|
+
const delivered = claim.contentDelivered ? "✅ delivered" : "⏳ pending delivery";
|
|
402
|
+
console.log(` 🤝 ${shortKey(claim.claimer)} — claimed ${formatTime(claim.claimedAt)} — ${delivered}`);
|
|
403
|
+
}
|
|
404
|
+
if (claims.length === 0) {
|
|
405
|
+
console.log(` (could not enumerate — use 'claims ${id}' with known wallets)`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const answererStr = prayer.answerer.toBase58();
|
|
410
|
+
if (answererStr !== "11111111111111111111111111111111") {
|
|
411
|
+
console.log("");
|
|
412
|
+
console.log(` Answerer: ${shortKey(prayer.answerer)}`);
|
|
338
413
|
}
|
|
339
414
|
const zeroHash = prayer.answerHash.every((b: number) => b === 0);
|
|
340
415
|
if (!zeroHash) {
|
|
341
|
-
console.log("");
|
|
342
416
|
console.log(` Answer Hash: ${hashToHex(prayer.answerHash)}`);
|
|
343
|
-
|
|
344
|
-
|
|
417
|
+
if (texts.answer) {
|
|
418
|
+
console.log(" Answer (decrypted):");
|
|
419
|
+
console.log(` ${texts.answer}`);
|
|
420
|
+
} else {
|
|
421
|
+
console.log(" Answer: 🔒 encrypted (not in local cache)");
|
|
422
|
+
}
|
|
345
423
|
console.log(` Fulfilled: ${formatTime(prayer.fulfilledAt)}`);
|
|
346
424
|
}
|
|
347
425
|
console.log("");
|
|
348
426
|
break;
|
|
349
427
|
}
|
|
350
428
|
|
|
429
|
+
case "claims": {
|
|
430
|
+
const id = parseInt(args[1]);
|
|
431
|
+
if (isNaN(id)) {
|
|
432
|
+
console.error("Usage: claims <prayer-id>");
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const prayer = await client.getPrayer(id);
|
|
437
|
+
if (!prayer) {
|
|
438
|
+
console.error(`\n✗ Prayer #${id} not found\n`);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.log("");
|
|
443
|
+
console.log(`🤝 Claims for Prayer #${id} (${prayer.numClaimers}/${prayer.maxClaimers})`);
|
|
444
|
+
console.log("═".repeat(50));
|
|
445
|
+
|
|
446
|
+
const claims = await client.getClaimsForPrayer(id);
|
|
447
|
+
if (claims.length === 0) {
|
|
448
|
+
console.log(" No claims found.");
|
|
449
|
+
} else {
|
|
450
|
+
for (const claim of claims) {
|
|
451
|
+
const delivered = claim.contentDelivered ? "✅ content delivered" : "⏳ awaiting delivery";
|
|
452
|
+
console.log(` 🤝 ${claim.claimer.toBase58()}`);
|
|
453
|
+
console.log(` Claimed: ${formatTime(claim.claimedAt)} | ${delivered}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
console.log("");
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
|
|
351
460
|
case "claim": {
|
|
352
461
|
const id = parseInt(args[1]);
|
|
353
462
|
if (isNaN(id)) {
|
|
@@ -358,6 +467,7 @@ async function main() {
|
|
|
358
467
|
try {
|
|
359
468
|
const tx = await client.claimPrayer(id);
|
|
360
469
|
console.log(` ✓ Claimed (tx: ${tx.slice(0, 16)}...)`);
|
|
470
|
+
console.log(` Waiting for requester to deliver encrypted content...`);
|
|
361
471
|
} catch (err: any) {
|
|
362
472
|
console.error(` ✗ ${err.message}`);
|
|
363
473
|
}
|
|
@@ -373,10 +483,11 @@ async function main() {
|
|
|
373
483
|
process.exit(1);
|
|
374
484
|
}
|
|
375
485
|
console.log(`\n💬 Answering prayer #${id}...`);
|
|
486
|
+
console.log(` 🔐 Encrypting answer for requester...`);
|
|
376
487
|
console.log(` Answer: ${answer.slice(0, 80)}${answer.length > 80 ? "..." : ""}`);
|
|
377
488
|
try {
|
|
378
489
|
const tx = await client.answerPrayer(id, answer);
|
|
379
|
-
console.log(` ✓ Answered (tx: ${tx.slice(0, 16)}...)`);
|
|
490
|
+
console.log(` ✓ Answered with encrypted reply (tx: ${tx.slice(0, 16)}...)`);
|
|
380
491
|
storeAnswer(id, answer);
|
|
381
492
|
} catch (err: any) {
|
|
382
493
|
console.error(` ✗ ${err.message}`);
|
|
@@ -394,7 +505,7 @@ async function main() {
|
|
|
394
505
|
console.log(`\n✅ Confirming prayer #${id}...`);
|
|
395
506
|
try {
|
|
396
507
|
const tx = await client.confirmPrayer(id);
|
|
397
|
-
console.log(` ✓ Confirmed (tx: ${tx.slice(0, 16)}...)`);
|
|
508
|
+
console.log(` ✓ Confirmed — bounty distributed to all claimers (tx: ${tx.slice(0, 16)}...)`);
|
|
398
509
|
} catch (err: any) {
|
|
399
510
|
console.error(` ✗ ${err.message}`);
|
|
400
511
|
}
|
|
@@ -422,12 +533,15 @@ async function main() {
|
|
|
422
533
|
case "unclaim": {
|
|
423
534
|
const id = parseInt(args[1]);
|
|
424
535
|
if (isNaN(id)) {
|
|
425
|
-
console.error("Usage: unclaim <prayer-id>");
|
|
536
|
+
console.error("Usage: unclaim <prayer-id> [--claimer <wallet>]");
|
|
426
537
|
process.exit(1);
|
|
427
538
|
}
|
|
428
|
-
|
|
539
|
+
const claimerArg = getArgValue("--claimer");
|
|
540
|
+
const claimerWallet = claimerArg ? new PublicKey(claimerArg) : undefined;
|
|
541
|
+
const target = claimerWallet ? shortKey(claimerWallet) : "self";
|
|
542
|
+
console.log(`\n🔓 Unclaiming prayer #${id} (${target})...`);
|
|
429
543
|
try {
|
|
430
|
-
const tx = await client.unclaimPrayer(id);
|
|
544
|
+
const tx = await client.unclaimPrayer(id, claimerWallet);
|
|
431
545
|
console.log(` ✓ Unclaimed (tx: ${tx.slice(0, 16)}...)`);
|
|
432
546
|
} catch (err: any) {
|
|
433
547
|
console.error(` ✗ ${err.message}`);
|
|
@@ -436,29 +550,62 @@ async function main() {
|
|
|
436
550
|
break;
|
|
437
551
|
}
|
|
438
552
|
|
|
553
|
+
case "close": {
|
|
554
|
+
const id = parseInt(args[1]);
|
|
555
|
+
if (isNaN(id)) {
|
|
556
|
+
console.error("Usage: close <prayer-id>");
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
console.log(`\n🗑️ Closing prayer #${id}...`);
|
|
560
|
+
try {
|
|
561
|
+
const tx = await client.closePrayer(id);
|
|
562
|
+
console.log(` ✓ Closed — rent returned (tx: ${tx.slice(0, 16)}...)`);
|
|
563
|
+
} catch (err: any) {
|
|
564
|
+
console.error(` ✗ ${err.message}`);
|
|
565
|
+
}
|
|
566
|
+
console.log("");
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
|
|
439
570
|
default:
|
|
440
571
|
console.log(`
|
|
441
|
-
🙏 Prayer Chain CLI
|
|
572
|
+
🙏 Prayer Chain CLI — Private by Default
|
|
573
|
+
All content E2E encrypted (X25519 + XSalsa20-Poly1305)
|
|
574
|
+
Supports multi-agent collaboration (1-10 claimers per prayer)
|
|
442
575
|
|
|
443
576
|
Commands:
|
|
444
|
-
chain
|
|
445
|
-
init
|
|
446
|
-
register "<name>" "<skills>"
|
|
447
|
-
agent [wallet]
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
--
|
|
451
|
-
--
|
|
577
|
+
chain Show prayer chain stats
|
|
578
|
+
init Initialize the prayer chain
|
|
579
|
+
register "<name>" "<skills>" Register (auto-derives encryption key)
|
|
580
|
+
agent [wallet] Show agent profile + encryption key
|
|
581
|
+
|
|
582
|
+
post "<content>" [options] Post a private prayer (hash-only on-chain)
|
|
583
|
+
--type <type> knowledge|compute|review|signal|collaboration
|
|
584
|
+
--bounty <SOL> SOL bounty (e.g. 0.01)
|
|
585
|
+
--ttl <seconds> Time to live (default 86400)
|
|
586
|
+
--claimers <n> Max collaborators (1-10, default 1)
|
|
587
|
+
|
|
452
588
|
list [--status <s>] [--limit <n>] List prayers
|
|
453
|
-
show <id>
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
589
|
+
show <id> Show prayer details + claims
|
|
590
|
+
claims <id> List all claims for a prayer
|
|
591
|
+
|
|
592
|
+
claim <id> Claim a prayer (creates Claim PDA)
|
|
593
|
+
deliver <id> [--claimer <wallet>] Deliver encrypted content (one or all)
|
|
594
|
+
answer <id> "<answer>" Answer with encrypted reply
|
|
595
|
+
confirm <id> Confirm — bounty splits among all claimers
|
|
596
|
+
cancel <id> Cancel an open prayer (0 claims only)
|
|
597
|
+
unclaim <id> [--claimer <wallet>] Remove a claim (self or expired)
|
|
598
|
+
close <id> Close resolved prayer, reclaim rent
|
|
599
|
+
|
|
600
|
+
Privacy:
|
|
601
|
+
🔐 No plaintext ever touches the blockchain
|
|
602
|
+
🔐 Content encrypted with DH shared secret (asker ↔ claimer)
|
|
603
|
+
🔐 Each claimer gets uniquely encrypted content delivery
|
|
604
|
+
🔐 Encryption key derived from your Solana wallet (no extra keys)
|
|
605
|
+
🔐 On-chain: only SHA-256 hashes + encrypted blobs in events
|
|
459
606
|
|
|
460
607
|
Environment:
|
|
461
|
-
SOLANA_RPC_URL
|
|
608
|
+
SOLANA_RPC_URL RPC endpoint (default: http://localhost:8899)
|
|
462
609
|
`.trim());
|
|
463
610
|
}
|
|
464
611
|
}
|