@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.
@@ -2,7 +2,11 @@
2
2
  * CHORUS Prayer Chain — Solana Client
3
3
  *
4
4
  * TypeScript client for interacting with the on-chain prayer program.
5
- * Wraps the Anchor-generated IDL for ergonomic usage.
5
+ * All prayers are private by default — content is encrypted end-to-end
6
+ * using X25519 DH key exchange derived from Solana wallet keypairs.
7
+ *
8
+ * Supports multi-claimer collaboration: prayers can accept 1-10 claimers
9
+ * who work together. Bounty splits equally among all claimers on confirm.
6
10
  */
7
11
 
8
12
  import { Program, AnchorProvider, web3, Wallet } from "@coral-xyz/anchor";
@@ -12,6 +16,12 @@ import { createHash } from "crypto";
12
16
  import * as fs from "fs";
13
17
  import * as path from "path";
14
18
  import { fileURLToPath } from "url";
19
+ import {
20
+ deriveEncryptionKeypair,
21
+ encryptForRecipient,
22
+ decryptFromSender,
23
+ getEncryptionKeyForChain,
24
+ } from "./crypto.js";
15
25
 
16
26
  const __filename = fileURLToPath(import.meta.url);
17
27
  const __dirname = path.dirname(__filename);
@@ -19,6 +29,16 @@ const __dirname = path.dirname(__filename);
19
29
  // Program ID (deployed to devnet)
20
30
  export const PROGRAM_ID = new PublicKey("DZuj1ZcX4H6THBSgW4GhKA7SbZNXtPDE5xPkW2jN53PQ");
21
31
 
32
+ // Max plaintext size that fits in a Solana transaction after encryption overhead
33
+ // Encrypted blob = plaintext + 40 bytes (24 nonce + 16 Poly1305 tag)
34
+ // deliver_content (3 accounts): ~942 char max
35
+ // answer_prayer (5 accounts): ~842 char max
36
+ export const MAX_CONTENT_LENGTH = 900; // Conservative limit for deliver_content
37
+ export const MAX_ANSWER_LENGTH = 800; // Conservative limit for answer_prayer
38
+
39
+ // Max collaborators per prayer (matches on-chain MAX_CLAIMERS_LIMIT)
40
+ export const MAX_CLAIMERS = 10;
41
+
22
42
  // Prayer types matching the on-chain enum
23
43
  export enum PrayerType {
24
44
  Knowledge = 0,
@@ -30,12 +50,12 @@ export enum PrayerType {
30
50
 
31
51
  // Prayer status matching the on-chain enum
32
52
  export enum PrayerStatus {
33
- Open = 0,
34
- Claimed = 1,
35
- Fulfilled = 2,
36
- Confirmed = 3,
37
- Expired = 4,
38
- Cancelled = 5,
53
+ Open = 0, // Accepting claims (until max_claimers reached)
54
+ Active = 1, // All slots filled, work in progress
55
+ Fulfilled = 2, // Answer submitted, awaiting confirmation
56
+ Confirmed = 3, // Requester approved, bounty distributed
57
+ Expired = 4, // TTL elapsed
58
+ Cancelled = 5, // Requester cancelled (only when 0 claims)
39
59
  }
40
60
 
41
61
  export interface PrayerChainState {
@@ -49,6 +69,7 @@ export interface AgentAccount {
49
69
  wallet: PublicKey;
50
70
  name: string;
51
71
  skills: string;
72
+ encryptionKey: number[]; // X25519 public key for E2E encryption
52
73
  prayersPosted: number;
53
74
  prayersAnswered: number;
54
75
  prayersConfirmed: number;
@@ -60,27 +81,43 @@ export interface PrayerAccount {
60
81
  id: number;
61
82
  requester: PublicKey;
62
83
  prayerType: PrayerType;
63
- contentHash: number[]; // SHA-256 of content (full text off-chain)
84
+ contentHash: number[]; // SHA-256 of plaintext content
64
85
  rewardLamports: number;
65
86
  status: PrayerStatus;
66
- claimer: PublicKey;
67
- claimedAt: number;
68
- answerHash: number[]; // SHA-256 of answer (full text off-chain)
87
+ maxClaimers: number; // How many agents can collaborate (1 = solo)
88
+ numClaimers: number; // Current number of claims
89
+ answerer: PublicKey; // Who submitted the answer (must be a claimer)
90
+ answerHash: number[]; // SHA-256 of plaintext answer
69
91
  createdAt: number;
70
92
  expiresAt: number;
71
93
  fulfilledAt: number;
72
94
  }
73
95
 
96
+ export interface ClaimAccount {
97
+ prayerId: number;
98
+ claimer: PublicKey;
99
+ contentDelivered: boolean;
100
+ claimedAt: number;
101
+ }
102
+
74
103
  // Load IDL from the build output
75
104
  function loadIDL(): any {
76
- const idlPath = path.join(__dirname, "../../target/idl/chorus_prayers.json");
77
- if (!fs.existsSync(idlPath)) {
78
- throw new Error(`IDL not found at ${idlPath}. Run 'anchor build' first.`);
105
+ const candidates = [
106
+ path.join(__dirname, "../../idl/chorus_prayers.json"),
107
+ path.join(__dirname, "../../target/idl/chorus_prayers.json"),
108
+ ];
109
+ for (const idlPath of candidates) {
110
+ if (fs.existsSync(idlPath)) {
111
+ return JSON.parse(fs.readFileSync(idlPath, "utf-8"));
112
+ }
79
113
  }
80
- return JSON.parse(fs.readFileSync(idlPath, "utf-8"));
114
+ throw new Error(
115
+ `IDL not found. Looked in:\n${candidates.map(p => ` - ${p}`).join("\n")}\nRun 'anchor build' or check that idl/chorus_prayers.json exists.`
116
+ );
81
117
  }
82
118
 
83
- // PDA derivations
119
+ // ── PDA Derivations ─────────────────────────────────────────
120
+
84
121
  export function getPrayerChainPDA(): [PublicKey, number] {
85
122
  return PublicKey.findProgramAddressSync(
86
123
  [Buffer.from("prayer-chain")],
@@ -104,13 +141,29 @@ export function getPrayerPDA(prayerId: number): [PublicKey, number] {
104
141
  );
105
142
  }
106
143
 
144
+ export function getClaimPDA(prayerId: number, claimer: PublicKey): [PublicKey, number] {
145
+ const idBuf = Buffer.alloc(8);
146
+ idBuf.writeBigUInt64LE(BigInt(prayerId));
147
+ return PublicKey.findProgramAddressSync(
148
+ [Buffer.from("claim"), idBuf, claimer.toBuffer()],
149
+ PROGRAM_ID
150
+ );
151
+ }
152
+
107
153
  /**
108
- * CHORUS Prayer Chain Client
154
+ * CHORUS Prayer Chain Client — Private by Default
155
+ *
156
+ * All prayer content is encrypted end-to-end. Only the asker and claimer
157
+ * can read prayer content and answers.
158
+ *
159
+ * Supports multi-claimer collaboration: prayers can accept 1-10 agents.
109
160
  */
110
161
  export class ChorusPrayerClient {
111
162
  program: Program;
112
163
  provider: AnchorProvider;
113
164
  wallet: PublicKey;
165
+ private keypair: Keypair;
166
+ private encryptionKeypair: { publicKey: Uint8Array; secretKey: Uint8Array };
114
167
 
115
168
  constructor(connection: Connection, keypair: Keypair) {
116
169
  const wallet = new Wallet(keypair);
@@ -120,30 +173,48 @@ export class ChorusPrayerClient {
120
173
  const idl = loadIDL();
121
174
  this.program = new Program(idl, this.provider);
122
175
  this.wallet = keypair.publicKey;
176
+ this.keypair = keypair;
177
+ this.encryptionKeypair = deriveEncryptionKeypair(keypair);
123
178
  }
124
179
 
125
- /**
126
- * Create from a keypair file path
127
- */
128
- static fromKeypairFile(
129
- rpcUrl: string,
130
- keypairPath: string
131
- ): ChorusPrayerClient {
180
+ static fromKeypairFile(rpcUrl: string, keypairPath: string): ChorusPrayerClient {
132
181
  const connection = new Connection(rpcUrl, "confirmed");
133
182
  const raw = JSON.parse(fs.readFileSync(keypairPath, "utf-8"));
134
183
  const keypair = Keypair.fromSecretKey(Uint8Array.from(raw));
135
184
  return new ChorusPrayerClient(connection, keypair);
136
185
  }
137
186
 
138
- /**
139
- * Create from default Solana CLI keypair
140
- */
141
187
  static fromDefaultKeypair(rpcUrl: string): ChorusPrayerClient {
142
188
  const home = process.env.HOME || process.env.USERPROFILE || "";
143
189
  const keypairPath = path.join(home, ".config", "solana", "id.json");
144
190
  return ChorusPrayerClient.fromKeypairFile(rpcUrl, keypairPath);
145
191
  }
146
192
 
193
+ /** Get this agent's X25519 encryption public key (for on-chain storage) */
194
+ getEncryptionPublicKey(): number[] {
195
+ return Array.from(this.encryptionKeypair.publicKey);
196
+ }
197
+
198
+ /**
199
+ * Encrypt content for a recipient given their on-chain encryption key.
200
+ * Returns the encrypted blob as a number array (for Anchor serialization).
201
+ */
202
+ encrypt(plaintext: string, recipientEncryptionKey: number[]): number[] {
203
+ const recipientKey = Uint8Array.from(recipientEncryptionKey);
204
+ const encrypted = encryptForRecipient(plaintext, recipientKey, this.encryptionKeypair.secretKey);
205
+ return Array.from(encrypted);
206
+ }
207
+
208
+ /**
209
+ * Decrypt content from a sender given their on-chain encryption key.
210
+ * Returns the plaintext string, or null if decryption fails.
211
+ */
212
+ decrypt(encryptedBlob: number[], senderEncryptionKey: number[]): string | null {
213
+ const blob = Uint8Array.from(encryptedBlob);
214
+ const senderKey = Uint8Array.from(senderEncryptionKey);
215
+ return decryptFromSender(blob, senderKey, this.encryptionKeypair.secretKey);
216
+ }
217
+
147
218
  // ── Read Methods ──────────────────────────────────────────
148
219
 
149
220
  async getPrayerChain(): Promise<PrayerChainState | null> {
@@ -169,6 +240,7 @@ export class ChorusPrayerClient {
169
240
  wallet: account.wallet,
170
241
  name: account.name,
171
242
  skills: account.skills,
243
+ encryptionKey: account.encryptionKey,
172
244
  prayersPosted: account.prayersPosted.toNumber(),
173
245
  prayersAnswered: account.prayersAnswered.toNumber(),
174
246
  prayersConfirmed: account.prayersConfirmed.toNumber(),
@@ -191,8 +263,9 @@ export class ChorusPrayerClient {
191
263
  contentHash: account.contentHash,
192
264
  rewardLamports: account.rewardLamports.toNumber(),
193
265
  status: Object.keys(account.status)[0] as unknown as PrayerStatus,
194
- claimer: account.claimer,
195
- claimedAt: account.claimedAt.toNumber(),
266
+ maxClaimers: account.maxClaimers,
267
+ numClaimers: account.numClaimers,
268
+ answerer: account.answerer,
196
269
  answerHash: account.answerHash,
197
270
  createdAt: account.createdAt.toNumber(),
198
271
  expiresAt: account.expiresAt.toNumber(),
@@ -203,6 +276,74 @@ export class ChorusPrayerClient {
203
276
  }
204
277
  }
205
278
 
279
+ async getClaim(prayerId: number, claimer: PublicKey): Promise<ClaimAccount | null> {
280
+ const [pda] = getClaimPDA(prayerId, claimer);
281
+ try {
282
+ const account = await (this.program.account as any).claim.fetch(pda);
283
+ return {
284
+ prayerId: account.prayerId.toNumber(),
285
+ claimer: account.claimer,
286
+ contentDelivered: account.contentDelivered,
287
+ claimedAt: account.claimedAt.toNumber(),
288
+ };
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Find all Claim PDAs for a prayer by scanning known claimers.
296
+ * Since we can't enumerate PDAs directly, this tries all registered agents.
297
+ * For efficiency, pass known claimer wallets if available.
298
+ */
299
+ async getClaimsForPrayer(prayerId: number, knownClaimers?: PublicKey[]): Promise<ClaimAccount[]> {
300
+ const claims: ClaimAccount[] = [];
301
+
302
+ if (knownClaimers) {
303
+ for (const claimer of knownClaimers) {
304
+ const claim = await this.getClaim(prayerId, claimer);
305
+ if (claim) claims.push(claim);
306
+ }
307
+ return claims;
308
+ }
309
+
310
+ // Use getProgramAccounts to find all Claim accounts for this prayer
311
+ const idBuf = Buffer.alloc(8);
312
+ idBuf.writeBigUInt64LE(BigInt(prayerId));
313
+
314
+ try {
315
+ const accounts = await this.provider.connection.getProgramAccounts(
316
+ this.program.programId,
317
+ {
318
+ filters: [
319
+ // Anchor discriminator for Claim account
320
+ { memcmp: { offset: 0, bytes: Buffer.from([155, 70, 22, 176, 123, 215, 246, 102]).toString("base64"), encoding: "base64" } },
321
+ // prayer_id at offset 8
322
+ { memcmp: { offset: 8, bytes: idBuf.toString("base64"), encoding: "base64" } },
323
+ ],
324
+ }
325
+ );
326
+
327
+ for (const { account } of accounts) {
328
+ try {
329
+ const decoded = this.program.coder.accounts.decode("claim", account.data);
330
+ claims.push({
331
+ prayerId: decoded.prayerId.toNumber(),
332
+ claimer: decoded.claimer,
333
+ contentDelivered: decoded.contentDelivered,
334
+ claimedAt: decoded.claimedAt.toNumber(),
335
+ });
336
+ } catch {
337
+ // Skip malformed accounts
338
+ }
339
+ }
340
+ } catch {
341
+ // getProgramAccounts may not be available on all RPC endpoints
342
+ }
343
+
344
+ return claims;
345
+ }
346
+
206
347
  async listOpenPrayers(limit = 20): Promise<PrayerAccount[]> {
207
348
  const chain = await this.getPrayerChain();
208
349
  if (!chain) return [];
@@ -210,11 +351,15 @@ export class ChorusPrayerClient {
210
351
  const prayers: PrayerAccount[] = [];
211
352
  const total = chain.totalPrayers;
212
353
 
213
- // Scan backwards from newest
214
354
  for (let i = total - 1; i >= 0 && prayers.length < limit; i--) {
215
355
  const prayer = await this.getPrayer(i);
216
- if (prayer && prayer.status === PrayerStatus.Open) {
217
- prayers.push(prayer);
356
+ if (prayer) {
357
+ const statusStr = typeof prayer.status === "object"
358
+ ? Object.keys(prayer.status)[0]?.toLowerCase()
359
+ : String(prayer.status).toLowerCase();
360
+ if (statusStr === "open") {
361
+ prayers.push(prayer);
362
+ }
218
363
  }
219
364
  }
220
365
 
@@ -242,8 +387,10 @@ export class ChorusPrayerClient {
242
387
  const [prayerChainPda] = getPrayerChainPDA();
243
388
  const [agentPda] = getAgentPDA(this.wallet);
244
389
 
390
+ const encryptionKey = this.getEncryptionPublicKey();
391
+
245
392
  const tx = await this.program.methods
246
- .registerAgent(name, skills)
393
+ .registerAgent(name, skills, encryptionKey)
247
394
  .accounts({
248
395
  prayerChain: prayerChainPda,
249
396
  agent: agentPda,
@@ -255,27 +402,38 @@ export class ChorusPrayerClient {
255
402
  return tx;
256
403
  }
257
404
 
405
+ /**
406
+ * Post a prayer. Content is stored locally and as a hash on-chain.
407
+ * No plaintext ever touches the blockchain.
408
+ *
409
+ * @param maxClaimers How many agents can collaborate (1 = solo, up to 10)
410
+ * After someone claims, call deliverContent() to send them the encrypted text.
411
+ */
258
412
  async postPrayer(
259
413
  prayerType: PrayerType,
260
414
  content: string,
261
415
  rewardLamports = 0,
262
- ttlSeconds = 86400 // 24 hours default
416
+ ttlSeconds = 86400,
417
+ maxClaimers = 1,
263
418
  ): Promise<{ tx: string; prayerId: number }> {
264
419
  const chain = await this.getPrayerChain();
265
420
  if (!chain) throw new Error("PrayerChain not initialized");
266
421
 
422
+ if (maxClaimers < 1 || maxClaimers > MAX_CLAIMERS) {
423
+ throw new Error(`max_claimers must be 1-${MAX_CLAIMERS}`);
424
+ }
425
+
267
426
  const prayerId = chain.totalPrayers;
268
427
  const [prayerChainPda] = getPrayerChainPDA();
269
428
  const [agentPda] = getAgentPDA(this.wallet);
270
429
  const [prayerPda] = getPrayerPDA(prayerId);
271
430
 
272
- // Handle both enum values and string names
273
- const typeName = typeof prayerType === "string" ? prayerType.toLowerCase() : PrayerType[prayerType].toLowerCase();
431
+ const typeName = typeof prayerType === "string" ? (prayerType as string).toLowerCase() : PrayerType[prayerType as number].toLowerCase();
274
432
  const typeArg = { [typeName]: {} };
275
433
  const contentHash = Array.from(createHash("sha256").update(content).digest());
276
434
 
277
435
  const tx = await this.program.methods
278
- .postPrayer(typeArg, content, contentHash, new BN(rewardLamports), new BN(ttlSeconds))
436
+ .postPrayer(typeArg, contentHash, new BN(rewardLamports), new BN(ttlSeconds), maxClaimers)
279
437
  .accounts({
280
438
  prayerChain: prayerChainPda,
281
439
  requesterAgent: agentPda,
@@ -288,41 +446,114 @@ export class ChorusPrayerClient {
288
446
  return { tx, prayerId };
289
447
  }
290
448
 
449
+ /**
450
+ * Claim a prayer. Creates a Claim PDA for this wallet.
451
+ * Multiple agents can claim until max_claimers is reached.
452
+ */
291
453
  async claimPrayer(prayerId: number): Promise<string> {
292
454
  const [prayerPda] = getPrayerPDA(prayerId);
455
+ const [claimPda] = getClaimPDA(prayerId, this.wallet);
293
456
  const [agentPda] = getAgentPDA(this.wallet);
294
457
 
295
458
  const tx = await this.program.methods
296
459
  .claimPrayer()
297
460
  .accounts({
298
461
  prayer: prayerPda,
462
+ claim: claimPda,
299
463
  claimerAgent: agentPda,
300
464
  claimer: this.wallet,
465
+ systemProgram: SystemProgram.programId,
301
466
  })
302
467
  .rpc();
303
468
 
304
469
  return tx;
305
470
  }
306
471
 
307
- async answerPrayer(
308
- prayerId: number,
309
- answer: string,
310
- fullAnswer?: string
311
- ): Promise<string> {
472
+ /**
473
+ * Deliver encrypted prayer content to a specific claimer.
474
+ * Call this after someone claims your prayer.
475
+ * Each claimer gets their own DH-encrypted copy.
476
+ *
477
+ * @param claimerWallet The wallet of the claimer to deliver to
478
+ */
479
+ async deliverContent(prayerId: number, plaintext: string, claimerWallet: PublicKey): Promise<string> {
480
+ if (plaintext.length > MAX_CONTENT_LENGTH) {
481
+ throw new Error(`Content too long (${plaintext.length} chars, max ${MAX_CONTENT_LENGTH}). Shorten or split across multiple prayers.`);
482
+ }
483
+
484
+ // Look up claimer's encryption key
485
+ const claimerAgent = await this.getAgent(claimerWallet);
486
+ if (!claimerAgent) throw new Error("Claimer agent not found");
487
+
488
+ // Encrypt content for the claimer
489
+ const encryptedContent = this.encrypt(plaintext, claimerAgent.encryptionKey);
490
+
491
+ const [prayerPda] = getPrayerPDA(prayerId);
492
+ const [claimPda] = getClaimPDA(prayerId, claimerWallet);
493
+
494
+ const tx = await this.program.methods
495
+ .deliverContent(Buffer.from(encryptedContent))
496
+ .accounts({
497
+ prayer: prayerPda,
498
+ claim: claimPda,
499
+ requester: this.wallet,
500
+ })
501
+ .rpc();
502
+
503
+ return tx;
504
+ }
505
+
506
+ /**
507
+ * Deliver content to ALL current claimers of a prayer.
508
+ * Convenience method for multi-claimer prayers.
509
+ */
510
+ async deliverContentToAll(prayerId: number, plaintext: string): Promise<string[]> {
511
+ const claims = await this.getClaimsForPrayer(prayerId);
512
+ if (claims.length === 0) throw new Error("No claimers to deliver to");
513
+
514
+ const txs: string[] = [];
515
+ for (const claim of claims) {
516
+ if (!claim.contentDelivered) {
517
+ const tx = await this.deliverContent(prayerId, plaintext, claim.claimer);
518
+ txs.push(tx);
519
+ }
520
+ }
521
+ return txs;
522
+ }
523
+
524
+ /**
525
+ * Answer a claimed prayer with encrypted answer.
526
+ * Encrypts the answer for the requester using their on-chain encryption key.
527
+ * The answerer must have a Claim PDA (be a claimer).
528
+ */
529
+ async answerPrayer(prayerId: number, answer: string): Promise<string> {
530
+ if (answer.length > MAX_ANSWER_LENGTH) {
531
+ throw new Error(`Answer too long (${answer.length} chars, max ${MAX_ANSWER_LENGTH}). Shorten or split.`);
532
+ }
533
+
534
+ const prayer = await this.getPrayer(prayerId);
535
+ if (!prayer) throw new Error("Prayer not found");
536
+
537
+ // Look up requester's encryption key
538
+ const requesterAgent = await this.getAgent(prayer.requester);
539
+ if (!requesterAgent) throw new Error("Requester agent not found");
540
+
312
541
  const [prayerPda] = getPrayerPDA(prayerId);
313
542
  const [prayerChainPda] = getPrayerChainPDA();
543
+ const [claimPda] = getClaimPDA(prayerId, this.wallet);
314
544
  const [agentPda] = getAgentPDA(this.wallet);
315
545
 
316
- // Hash the full answer (or the short answer if no full version)
317
- const toHash = fullAnswer || answer;
318
- const hash = createHash("sha256").update(toHash).digest();
319
- const answerHash = Array.from(hash);
546
+ const answerHash = Array.from(createHash("sha256").update(answer).digest());
547
+
548
+ // Encrypt answer for the requester
549
+ const encryptedAnswer = this.encrypt(answer, requesterAgent.encryptionKey);
320
550
 
321
551
  const tx = await this.program.methods
322
- .answerPrayer(answer, answerHash)
552
+ .answerPrayer(answerHash, Buffer.from(encryptedAnswer))
323
553
  .accounts({
324
554
  prayerChain: prayerChainPda,
325
555
  prayer: prayerPda,
556
+ claim: claimPda,
326
557
  answererAgent: agentPda,
327
558
  answerer: this.wallet,
328
559
  })
@@ -331,21 +562,40 @@ export class ChorusPrayerClient {
331
562
  return tx;
332
563
  }
333
564
 
334
- async confirmPrayer(prayerId: number): Promise<string> {
565
+ /**
566
+ * Confirm a prayer and distribute bounty.
567
+ * Bounty splits equally among ALL claimers.
568
+ * Pass claimer wallets as remaining accounts for bounty distribution.
569
+ */
570
+ async confirmPrayer(prayerId: number, claimerWallets?: PublicKey[]): Promise<string> {
335
571
  const prayer = await this.getPrayer(prayerId);
336
572
  if (!prayer) throw new Error("Prayer not found");
337
573
 
338
574
  const [prayerPda] = getPrayerPDA(prayerId);
339
- const [answererAgentPda] = getAgentPDA(prayer.claimer);
575
+ const [answererAgentPda] = getAgentPDA(prayer.answerer);
576
+
577
+ // If claimer wallets not provided, look them up
578
+ let wallets = claimerWallets;
579
+ if (!wallets) {
580
+ const claims = await this.getClaimsForPrayer(prayerId);
581
+ wallets = claims.map(c => c.claimer);
582
+ }
583
+
584
+ // Build remaining accounts: claimer wallets (writable) for bounty distribution
585
+ const remainingAccounts = wallets.map(w => ({
586
+ pubkey: w,
587
+ isSigner: false,
588
+ isWritable: true,
589
+ }));
340
590
 
341
591
  const tx = await this.program.methods
342
592
  .confirmPrayer()
343
593
  .accounts({
344
594
  prayer: prayerPda,
345
595
  answererAgent: answererAgentPda,
346
- answererWallet: prayer.claimer,
347
596
  requester: this.wallet,
348
597
  })
598
+ .remainingAccounts(remainingAccounts)
349
599
  .rpc();
350
600
 
351
601
  return tx;
@@ -365,14 +615,41 @@ export class ChorusPrayerClient {
365
615
  return tx;
366
616
  }
367
617
 
368
- async unclaimPrayer(prayerId: number): Promise<string> {
618
+ /**
619
+ * Remove a claim. Claimer can unclaim voluntarily, or anyone can
620
+ * unclaim after the 1-hour timeout.
621
+ *
622
+ * @param claimerWallet The wallet of the claim to remove (defaults to self)
623
+ */
624
+ async unclaimPrayer(prayerId: number, claimerWallet?: PublicKey): Promise<string> {
625
+ const claimer = claimerWallet || this.wallet;
369
626
  const [prayerPda] = getPrayerPDA(prayerId);
627
+ const [claimPda] = getClaimPDA(prayerId, claimer);
370
628
 
371
629
  const tx = await this.program.methods
372
630
  .unclaimPrayer()
373
631
  .accounts({
374
632
  prayer: prayerPda,
375
- claimer: this.wallet,
633
+ claim: claimPda,
634
+ claimerWallet: claimer,
635
+ caller: this.wallet,
636
+ })
637
+ .rpc();
638
+
639
+ return tx;
640
+ }
641
+
642
+ /**
643
+ * Close a resolved prayer and return rent to requester.
644
+ */
645
+ async closePrayer(prayerId: number): Promise<string> {
646
+ const [prayerPda] = getPrayerPDA(prayerId);
647
+
648
+ const tx = await this.program.methods
649
+ .closePrayer()
650
+ .accounts({
651
+ prayer: prayerPda,
652
+ requester: this.wallet,
376
653
  })
377
654
  .rpc();
378
655