solana-messenger-sdk 1.2.1 → 1.4.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/GUIDE.md ADDED
@@ -0,0 +1,753 @@
1
+ # Integration Guide — Building on Solana Messenger
2
+
3
+ This guide walks you through every operation the protocol supports, with code examples and explanations of when and why you'd use each one.
4
+
5
+ **Program:** `msg1SxLsvf1ZL374noHwUWcVYjPsNSNwKb3xphg6Lxf` (mainnet & devnet)
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Setup](#setup)
10
+ 2. [Identity & Registration](#identity--registration)
11
+ 3. [Direct Messages](#direct-messages)
12
+ 4. [Servers](#servers)
13
+ 5. [Channels](#channels)
14
+ 6. [Discovery](#discovery)
15
+ 7. [Key Management](#key-management)
16
+ 8. [Account Deserialization](#account-deserialization)
17
+ 9. [Common Patterns](#common-patterns)
18
+ 10. [Architecture Reference](#architecture-reference)
19
+
20
+ ---
21
+
22
+ ## Setup
23
+
24
+ ```typescript
25
+ import { SolanaMessenger, ChannelType } from "solana-messenger-sdk";
26
+ import { readFileSync } from "fs";
27
+
28
+ // Option 1: Self-custody (you have a keypair file)
29
+ const keypair = new Uint8Array(JSON.parse(readFileSync("~/.config/solana/id.json", "utf-8")));
30
+ const messenger = new SolanaMessenger({
31
+ apiKey: "YOUR_HELIUS_API_KEY",
32
+ network: "mainnet", // or "devnet"
33
+ keypair,
34
+ });
35
+
36
+ // Option 2: Custodial wallet (Privy, Turnkey, etc.)
37
+ const messenger = new SolanaMessenger({
38
+ apiKey: "YOUR_HELIUS_API_KEY",
39
+ walletAddress: "YourCustodialWalletAddress",
40
+ signer: async (unsignedTx, recentBlockhash, feePayer) => {
41
+ return await yourCustodialProvider.signTransaction(unsignedTx);
42
+ },
43
+ });
44
+
45
+ // Always call init() first — generates encryption key, registers on-chain
46
+ const { encryptionAddress, status } = await messenger.init();
47
+ // status: "registered" | "already_registered" | "updated"
48
+ ```
49
+
50
+ **Why two keys?** Your wallet key (A) signs transactions and can be custodial. Your encryption key (B) is generated locally and never leaves your machine. This means Privy/Turnkey can sign transactions for you but can never read your messages.
51
+
52
+ ---
53
+
54
+ ## Identity & Registration
55
+
56
+ Every user needs an on-chain encryption key registry before they can send or receive messages.
57
+
58
+ ### Register (automatic via init)
59
+
60
+ ```typescript
61
+ await messenger.init(); // handles registration automatically
62
+ ```
63
+
64
+ `init()` does three things:
65
+ 1. Generates a local encryption keypair (stored at `~/.solana-messenger/keys/<address>.json`)
66
+ 2. Registers the public key on-chain at PDA `["messenger", walletAddress]`
67
+ 3. Loads platform config (fee vault, protocol fee)
68
+
69
+ ### Set spam fee
70
+
71
+ **When:** You want senders to pay you a fee to send DMs (spam deterrent).
72
+
73
+ ```typescript
74
+ await messenger.setMinFee(10000); // 10,000 lamports (~$0.001) per DM to you
75
+ ```
76
+
77
+ Senders automatically pay this fee when calling `send()`. It goes directly to your wallet.
78
+
79
+ ### Look up someone's encryption key
80
+
81
+ **When:** You want to verify someone is registered, or you're building custom encryption.
82
+
83
+ ```typescript
84
+ const encKey = await messenger.lookupEncryptionKey("SomeWalletAddress");
85
+ if (!encKey) console.log("User not registered — can't message them");
86
+ ```
87
+
88
+ ### Deregister
89
+
90
+ **When:** You're done with the protocol and want to reclaim rent (~0.001 SOL).
91
+
92
+ ```typescript
93
+ await messenger.deregister();
94
+ ```
95
+
96
+ ⚠️ After deregistering, nobody can send you messages until you re-register.
97
+
98
+ ---
99
+
100
+ ## Direct Messages
101
+
102
+ Wallet-to-wallet encrypted messages. Permanent, protocol-level, nobody can revoke them.
103
+
104
+ ### Send a DM
105
+
106
+ ```typescript
107
+ const signatures = await messenger.send("RecipientAddress", "Hey, what's up?");
108
+ // Returns array of tx signatures (multiple if message was chunked)
109
+ ```
110
+
111
+ The SDK:
112
+ 1. Looks up recipient's encryption key from the registry
113
+ 2. Performs X25519 Diffie-Hellman key exchange
114
+ 3. Encrypts with NaCl box (XSalsa20-Poly1305)
115
+ 4. Auto-chunks messages > 661 bytes
116
+ 5. Pays protocol fee + recipient min_fee automatically
117
+
118
+ ### Read DMs
119
+
120
+ ```typescript
121
+ const messages = await messenger.read({ limit: 20, since: 1710000000 });
122
+ for (const msg of messages) {
123
+ console.log(`[${new Date(msg.timestamp * 1000).toISOString()}] ${msg.sender}: ${msg.text}`);
124
+ }
125
+ ```
126
+
127
+ ### Listen for DMs in real-time
128
+
129
+ ```typescript
130
+ const unsubscribe = await messenger.listen((msg) => {
131
+ console.log(`New DM from ${msg.sender}: ${msg.text}`);
132
+ });
133
+
134
+ // Later: stop listening
135
+ unsubscribe();
136
+ ```
137
+
138
+ Uses WebSocket subscription — ~400ms latency.
139
+
140
+ ---
141
+
142
+ ## Servers
143
+
144
+ Servers are workspaces (like Discord servers or Slack workspaces). They contain channels and members with roles.
145
+
146
+ ### Create a server
147
+
148
+ **When:** You want to create a workspace for a team, community, or organization.
149
+
150
+ ```typescript
151
+ const { serverId, serverPda, signature } = await messenger.createServer("My Team");
152
+ ```
153
+
154
+ What happens:
155
+ 1. Generates a random server ID (keypair pubkey)
156
+ 2. Creates Server PDA at `["server", serverId]`
157
+ 3. Creates your ServerMember PDA at `["server_member", serverId, yourWallet]` with role=Owner
158
+ 4. Generates the first server encryption key (version 0) and wraps it for you
159
+
160
+ You're now the **Owner** — full control over the server.
161
+
162
+ ### Invite a member
163
+
164
+ **When:** You want to add someone to your server. You must be Admin or Owner.
165
+
166
+ ```typescript
167
+ // You need to know the current server keys to wrap them for the new member
168
+ const myMember = await messenger.getServerMember(serverId);
169
+ // Unwrap your server keys first
170
+ const serverKeys = myMember.keyWraps.map(kw => {
171
+ const key = messenger.unwrapKey(kw.wrappedKey, kw.nonce, adminEncryptionPubkey);
172
+ if (!key) throw new Error("Failed to unwrap key");
173
+ return key;
174
+ });
175
+
176
+ await messenger.inviteToServer(serverId, "NewMemberWallet", serverKeys);
177
+ ```
178
+
179
+ What happens:
180
+ 1. Verifies you're Admin/Owner (checks your ServerMember PDA)
181
+ 2. Creates a ServerMember for the new member
182
+ 3. Wraps ALL historical server key versions for them (so they can read full history)
183
+ 4. New member gets role=Member
184
+
185
+ ### Remove a member
186
+
187
+ **When:** Someone needs to be kicked. Admin/Owner only. **Rotates the server key** so the removed member can't read future messages.
188
+
189
+ ```typescript
190
+ // Get current member list
191
+ const members = await messenger.getServerMembers(serverId);
192
+ const remaining = members
193
+ .filter(m => m.account.member !== "RemovedMemberWallet")
194
+ .map(m => m.account.member);
195
+
196
+ await messenger.removeServerMember(serverId, "RemovedMemberWallet", remaining);
197
+ ```
198
+
199
+ What happens:
200
+ 1. Closes the removed member's ServerMember PDA (reclaims rent)
201
+ 2. Generates a new server key (version N+1)
202
+ 3. Wraps the new key for all remaining members
203
+ 4. Removed member still has old keys cached locally (can read old messages) but can't decrypt anything new
204
+
205
+ ### Leave a server
206
+
207
+ **When:** You want to leave voluntarily. Also rotates the key (forward secrecy).
208
+
209
+ ```typescript
210
+ const members = await messenger.getServerMembers(serverId);
211
+ const remaining = members
212
+ .filter(m => m.account.member !== myAddress)
213
+ .map(m => m.account.member);
214
+
215
+ await messenger.leaveServer(serverId, remaining);
216
+ ```
217
+
218
+ ⚠️ The server **owner** cannot leave — transfer ownership first (not yet in SDK, use low-level builders).
219
+
220
+ ### Update server name
221
+
222
+ ```typescript
223
+ await messenger.updateServer(serverId, "New Server Name");
224
+ ```
225
+
226
+ ### Close a server
227
+
228
+ **When:** You're done with the server entirely. Owner only. Reclaims all rent.
229
+
230
+ ```typescript
231
+ const members = await messenger.getServerMembers(serverId);
232
+ const memberAddresses = members.map(m => m.account.member);
233
+ await messenger.closeServer(serverId, memberAddresses);
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Channels
239
+
240
+ Channels are where messages are sent. Two types:
241
+
242
+ | Type | Key Source | Members | Use Case |
243
+ |------|-----------|---------|----------|
244
+ | **Public** | Server key | All server members | #general, #announcements |
245
+ | **Private** | Own channel key | Invite-only | DM groups, private discussions |
246
+
247
+ Channels can be **server channels** (belong to a server) or **standalone** (group DMs without a server).
248
+
249
+ ### Create a public channel (in a server)
250
+
251
+ **When:** You want a channel visible to all server members. Messages encrypted with the server key.
252
+
253
+ ```typescript
254
+ const { channelId, channelPda } = await messenger.createChannel({
255
+ serverId: serverId,
256
+ channelType: ChannelType.Public,
257
+ members: [], // public channels don't need a member list
258
+ });
259
+ ```
260
+
261
+ ### Create a private channel (in a server)
262
+
263
+ **When:** You want an invite-only channel within a server. Has its own encryption key.
264
+
265
+ ```typescript
266
+ const { channelId, channelPda } = await messenger.createChannel({
267
+ serverId: serverId,
268
+ channelType: ChannelType.Private,
269
+ members: [myAddress, member1, member2],
270
+ });
271
+ ```
272
+
273
+ ### Create a standalone group DM (no server)
274
+
275
+ **When:** You want a group chat without the overhead of a full server.
276
+
277
+ ```typescript
278
+ const { channelId, channelPda } = await messenger.createChannel({
279
+ members: [myAddress, friend1, friend2], // you must be in the list
280
+ });
281
+ ```
282
+
283
+ ### Send a message to a channel
284
+
285
+ **When:** Sending to any channel (public or private).
286
+
287
+ ```typescript
288
+ // For PUBLIC channels: use the server key
289
+ const myServerMember = await messenger.getServerMember(serverId);
290
+ const serverKey = messenger.unwrapKey(
291
+ myServerMember.keyWraps[keyVersion].wrappedKey,
292
+ myServerMember.keyWraps[keyVersion].nonce,
293
+ wrapperEncryptionPubkey,
294
+ );
295
+ const senderServerMemberPda = await deriveServerMemberPda(serverId, myAddress, programId);
296
+
297
+ await messenger.sendChannelMessage({
298
+ channelPda,
299
+ message: "Hello everyone!",
300
+ channelKey: serverKey,
301
+ keyVersion: myServerMember.keyWraps.length - 1, // latest version
302
+ senderServerMemberPda, // REQUIRED for public channels
303
+ });
304
+
305
+ // For PRIVATE channels: use the channel key
306
+ const myChannelMember = await messenger.getChannelMember(channelId);
307
+ const channelKey = messenger.unwrapKey(
308
+ myChannelMember.keyWraps[0].wrappedKey,
309
+ myChannelMember.keyWraps[0].nonce,
310
+ wrapperEncryptionPubkey,
311
+ );
312
+
313
+ await messenger.sendChannelMessage({
314
+ channelPda,
315
+ message: "Private discussion",
316
+ channelKey: channelKey,
317
+ keyVersion: 0,
318
+ // no senderServerMemberPda needed for private channels
319
+ });
320
+ ```
321
+
322
+ **Key difference:** Public channels require `senderServerMemberPda` — the program verifies you're a server member before accepting the message.
323
+
324
+ ### Read channel messages
325
+
326
+ ```typescript
327
+ const messages = await messenger.readChannelMessages(channelId, channelKey, {
328
+ limit: 50,
329
+ since: 1710000000, // unix timestamp
330
+ });
331
+
332
+ for (const msg of messages) {
333
+ console.log(`[${msg.sender}] ${msg.text} (key v${msg.keyVersion})`);
334
+ }
335
+ ```
336
+
337
+ ### Listen for channel messages in real-time
338
+
339
+ ```typescript
340
+ const unsubscribe = await messenger.listenChannel(channelPda, channelKey, (msg) => {
341
+ console.log(`${msg.sender}: ${msg.text}`);
342
+ });
343
+ ```
344
+
345
+ ### Add members to a private channel
346
+
347
+ **When:** Inviting new people to an existing private channel. Creator only.
348
+
349
+ ```typescript
350
+ // New members get ALL historical key versions (Slack-style full history)
351
+ await messenger.addChannelMembers(channelPda, channelId, ["NewMember"], [allChannelKeys]);
352
+ ```
353
+
354
+ ### Remove members from a private channel
355
+
356
+ **When:** Kicking someone. Rotates the channel key. Creator only.
357
+
358
+ ```typescript
359
+ const { newChannelPda } = await messenger.removeChannelMembers(
360
+ channelId,
361
+ currentVersion, // current channel version (from channel account)
362
+ ["RemovedMember"], // who to remove
363
+ ["Remaining1", "Remaining2"], // who stays
364
+ );
365
+ // ⚠️ Channel PDA changes! Old PDA is closed, new one at version+1
366
+ ```
367
+
368
+ ### Close a channel
369
+
370
+ ```typescript
371
+ await messenger.closeChannel(channelPda, channelId, memberAddresses);
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Discovery
377
+
378
+ **The key promise:** With just your wallet key + encryption key, you can discover everything you're part of — no backend needed.
379
+
380
+ ### Find all my servers
381
+
382
+ ```typescript
383
+ const servers = await messenger.getMyServers();
384
+ for (const { server, account } of servers) {
385
+ const roleName = ["Member", "Admin", "Owner"][account.role];
386
+ console.log(`${server.name} — ${roleName} (joined ${new Date(account.joinedAt * 1000).toISOString()})`);
387
+ }
388
+ ```
389
+
390
+ ### Find all my private channels
391
+
392
+ ```typescript
393
+ const channels = await messenger.getMyChannels();
394
+ for (const { channel, account } of channels) {
395
+ if (channel) {
396
+ const type = channel.channelType === 0 ? "Private" : "Public";
397
+ console.log(`Channel ${channel.channelId} (${type}, ${channel.members.length} members)`);
398
+ }
399
+ }
400
+ ```
401
+
402
+ ### Find all channels in a server
403
+
404
+ ```typescript
405
+ const channels = await messenger.getServerChannels(serverId);
406
+ for (const { account } of channels) {
407
+ const type = account.channelType === 0 ? "Private" : "Public";
408
+ console.log(`${account.channelId} — ${type}, v${account.version}`);
409
+ }
410
+ ```
411
+
412
+ ### Find all members of a server
413
+
414
+ ```typescript
415
+ const members = await messenger.getServerMembers(serverId);
416
+ for (const { account } of members) {
417
+ const roleName = ["Member", "Admin", "Owner"][account.role];
418
+ console.log(`${account.member} — ${roleName}`);
419
+ }
420
+ ```
421
+
422
+ ### Get a specific server/channel/member
423
+
424
+ ```typescript
425
+ const server = await messenger.getServer(serverId);
426
+ const channel = await messenger.getChannel(channelId, version);
427
+ const myServerMember = await messenger.getServerMember(serverId);
428
+ const myChannelMember = await messenger.getChannelMember(channelId);
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Key Management
434
+
435
+ The encryption model uses **shared symmetric keys** wrapped per-member.
436
+
437
+ ### How it works
438
+
439
+ 1. **Server key**: One symmetric key per server version. Stored wrapped in each ServerMember's `keyWraps`.
440
+ 2. **Channel key**: One symmetric key per channel version (private channels only). Stored in ChannelMember's `keyWraps`.
441
+ 3. **Key rotation**: When a member is removed (or leaves), a NEW key is generated and wrapped for all remaining members. The old key still exists in their records (for reading old messages).
442
+ 4. **`keyVersion`**: When sending a message, you specify which key version you used. Readers use this to find the right key wrap to decrypt.
443
+
444
+ ### Unwrap a key
445
+
446
+ ```typescript
447
+ // Get your ServerMember or ChannelMember account
448
+ const myMember = await messenger.getServerMember(serverId);
449
+
450
+ // Unwrap the latest key
451
+ const latestWrap = myMember.keyWraps[myMember.keyWraps.length - 1];
452
+ const key = messenger.unwrapKey(
453
+ latestWrap.wrappedKey,
454
+ latestWrap.nonce,
455
+ wrapperEncryptionPubkey, // encryption pubkey of whoever wrapped it (admin/creator)
456
+ );
457
+ ```
458
+
459
+ ### Who wrapped the key?
460
+
461
+ The wrapper is whoever called `inviteToServer`, `createServer`, `removeServerMember`, or `leaveServer`. You need their **encryption public key** (from the EncryptionRegistry) to unwrap.
462
+
463
+ For key wraps created by different people at different times, you may need to try multiple encryption pubkeys. In practice, most servers have one admin who does all invites.
464
+
465
+ ### Key version timeline example
466
+
467
+ ```
468
+ v0: Server created with key K0 (wrapped for owner)
469
+ → invite member B (K0 wrapped for B)
470
+ → invite member C (K0 wrapped for C)
471
+ v1: Member C removed → new key K1 (wrapped for owner + B)
472
+ → C can still read v0 messages, not v1+
473
+ v2: Member B leaves → new key K2 (wrapped for owner only)
474
+ → B can still read v0-v1 messages, not v2+
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Account Deserialization
480
+
481
+ For custom integrations, you can deserialize raw account data directly:
482
+
483
+ ```typescript
484
+ import {
485
+ deserializeServer,
486
+ deserializeServerMember,
487
+ deserializeChannel,
488
+ deserializeChannelMember,
489
+ deserializeEncryptionRegistry,
490
+ deserializePlatformConfig,
491
+ ACCOUNT_DISCRIMINATORS,
492
+ } from "solana-messenger-sdk";
493
+
494
+ // From raw RPC data
495
+ const accountInfo = await rpc.getAccountInfo(address(pda), { encoding: "base64" }).send();
496
+ const data = Buffer.from(accountInfo.value.data[0], "base64");
497
+ const server = deserializeServer(new Uint8Array(data));
498
+ ```
499
+
500
+ ### Account layouts
501
+
502
+ | Account | PDA Seeds | Key Fields |
503
+ |---------|-----------|------------|
504
+ | `Server` | `["server", serverId]` | serverId, owner, name |
505
+ | `ServerMember` | `["server_member", serverId, member]` | serverId, member, role, keyWraps |
506
+ | `Channel` | `["channel", channelId, versionBytes]` | channelId, serverId?, creator, version, channelType, members |
507
+ | `ChannelMember` | `["channel_member", channelId, member]` | channelId, member, keyWraps |
508
+ | `EncryptionRegistry` | `["messenger", owner]` | owner, encryptionKey, minFee |
509
+ | `PlatformConfig` | `["config"]` | authority, feeVault, protocolFee |
510
+
511
+ ---
512
+
513
+ ## WebSocket Strategy
514
+
515
+ The SDK offers multiple real-time listening strategies depending on your use case.
516
+
517
+ ### Single-channel listening
518
+
519
+ Subscribe to one channel's PDA — ideal when you only care about one conversation.
520
+
521
+ ```typescript
522
+ const unsubscribe = await messenger.listenChannel(channelPda, channelKey, (msg) => {
523
+ console.log(`${msg.sender}: ${msg.text}`);
524
+ });
525
+ ```
526
+
527
+ Pros: Simple, auto-decrypts messages for you.
528
+ Cons: One WebSocket per channel. Doesn't scale to many channels.
529
+
530
+ ### Server-wide listening
531
+
532
+ Subscribe to all public channels in a server at once. Each channel gets its own WebSocket subscription (typically 10-50 per server — very manageable).
533
+
534
+ **Why not program-wide?** Subscribing to the program address means receiving EVERY message from EVERY server globally. At scale (10,000+ servers), that's a firehose of data you don't need and a waste of RPC credits.
535
+
536
+ ```typescript
537
+ // Unwrap all your server key versions
538
+ const myMember = await messenger.getServerMember(serverId);
539
+ const serverKeys = new Map<number, Uint8Array>();
540
+ for (const kw of myMember.keyWraps) {
541
+ const key = messenger.unwrapKey(kw.wrappedKey, kw.nonce, wrapperEncPubkey);
542
+ if (key) serverKeys.set(kw.version, key);
543
+ }
544
+
545
+ // Listen to all public channels in this server
546
+ const unsubscribe = await messenger.listenServer(serverId, serverKeys, (msg) => {
547
+ console.log(`[${msg.channelId}] ${msg.sender}: ${msg.text}`);
548
+ });
549
+ ```
550
+
551
+ ### DM listening
552
+
553
+ Subscribe to your wallet address — only YOUR DMs, not everyone's.
554
+
555
+ ```typescript
556
+ const unsubDM = await messenger.listen((msg) => {
557
+ console.log(`DM from ${msg.sender}: ${msg.text}`);
558
+ });
559
+ ```
560
+
561
+ ### Pattern: Slack-like real-time
562
+
563
+ The recommended approach for a full messaging app:
564
+
565
+ 1. **DMs**: one `listen()` call (subscribes to your wallet)
566
+ 2. **Per-server**: one `listenServer()` call per server you're in (subscribes to that server's channel PDAs)
567
+ 3. **Private channels**: one `listenChannel()` per private channel (they have separate keys)
568
+
569
+ ```typescript
570
+ // 1. DMs
571
+ const unsubDM = await messenger.listen((msg) => renderDM(msg));
572
+
573
+ // 2. For each server, listen to public channels
574
+ const serverUnsubs: (() => void)[] = [];
575
+ const servers = await messenger.getMyServers();
576
+ for (const { server, account } of servers) {
577
+ const serverKeys = new Map<number, Uint8Array>();
578
+ for (const kw of account.keyWraps) {
579
+ const key = messenger.unwrapKey(kw.wrappedKey, kw.nonce, wrapperPubkey);
580
+ if (key) serverKeys.set(kw.version, key);
581
+ }
582
+ const unsub = await messenger.listenServer(server.serverId, serverKeys, (msg) => {
583
+ renderChannelMessage(msg);
584
+ });
585
+ serverUnsubs.push(unsub);
586
+ }
587
+
588
+ // 3. Private channels (separate keys)
589
+ const privateUnsubs: (() => void)[] = [];
590
+ const myChannels = await messenger.getMyChannels();
591
+ for (const { account, channel } of myChannels) {
592
+ if (!channel || channel.channelType !== 0) continue; // skip public
593
+ const latestWrap = account.keyWraps[account.keyWraps.length - 1];
594
+ const key = messenger.unwrapKey(latestWrap.wrappedKey, latestWrap.nonce, wrapperPubkey);
595
+ if (!key) continue;
596
+ const channelPda = await deriveChannelPda(channel.channelId, channel.version, programId);
597
+ const unsub = await messenger.listenChannel(channelPda, key, (msg) => {
598
+ renderChannelMessage(msg);
599
+ });
600
+ privateUnsubs.push(unsub);
601
+ }
602
+
603
+ // Clean up
604
+ unsubDM();
605
+ serverUnsubs.forEach(fn => fn());
606
+ privateUnsubs.forEach(fn => fn());
607
+ ```
608
+
609
+ **Subscription count for a typical workspace:**
610
+ - 1 WebSocket for DMs
611
+ - 10-50 WebSockets per server (one per public channel)
612
+ - 1-5 WebSockets for private channels
613
+
614
+ Totals ~20-60 subscriptions — well within Helius limits.
615
+
616
+ ---
617
+
618
+ ## Common Patterns
619
+
620
+ ### Pattern: "Show me everything" (first login / new device)
621
+
622
+ ```typescript
623
+ await messenger.init();
624
+
625
+ // 1. Find all servers
626
+ const servers = await messenger.getMyServers();
627
+
628
+ // 2. For each server, find channels
629
+ for (const { server, account } of servers) {
630
+ const channels = await messenger.getServerChannels(server.serverId);
631
+ console.log(`Server: ${server.name} (${channels.length} channels)`);
632
+ }
633
+
634
+ // 3. Find standalone group DMs
635
+ const myChannels = await messenger.getMyChannels();
636
+ const groupDMs = myChannels.filter(c => c.channel && !c.channel.serverId);
637
+
638
+ // 4. DM history
639
+ const dms = await messenger.read({ limit: 50 });
640
+ ```
641
+
642
+ ### Pattern: Full server setup
643
+
644
+ ```typescript
645
+ // 1. Create server
646
+ const { serverId } = await messenger.createServer("Acme Corp");
647
+
648
+ // 2. Create channels
649
+ await messenger.createChannel({ serverId, channelType: ChannelType.Public, members: [] }); // #general
650
+ await messenger.createChannel({ serverId, channelType: ChannelType.Private, members: [myAddr, cofounderAddr] }); // #founders
651
+
652
+ // 3. Invite team
653
+ const myMember = await messenger.getServerMember(serverId);
654
+ const serverKey = messenger.unwrapKey(myMember.keyWraps[0].wrappedKey, myMember.keyWraps[0].nonce, myEncPubkey);
655
+ await messenger.inviteToServer(serverId, "TeamMember1", [serverKey]);
656
+ await messenger.inviteToServer(serverId, "TeamMember2", [serverKey]);
657
+ ```
658
+
659
+ ### Pattern: Offboarding a member
660
+
661
+ ```typescript
662
+ // 1. Remove from server (rotates server key → public channels secured)
663
+ const members = await messenger.getServerMembers(serverId);
664
+ const remaining = members.filter(m => m.account.member !== removedAddr).map(m => m.account.member);
665
+ await messenger.removeServerMember(serverId, removedAddr, remaining);
666
+
667
+ // 2. Remove from private channels (must do separately!)
668
+ const channels = await messenger.getServerChannels(serverId);
669
+ for (const { account: ch } of channels) {
670
+ if (ch.channelType === 0 && ch.members.includes(removedAddr)) {
671
+ const chRemaining = ch.members.filter(m => m !== removedAddr);
672
+ await messenger.removeChannelMembers(ch.channelId, ch.version, [removedAddr], chRemaining);
673
+ }
674
+ }
675
+ ```
676
+
677
+ ⚠️ **Important:** Removing from a server does NOT automatically remove from private channels. You must do both.
678
+
679
+ ### Pattern: Real-time Slack-like app
680
+
681
+ See the full example in the [WebSocket Strategy](#websocket-strategy) section above — it covers DMs, per-server listening, and private channel listening with proper key management.
682
+
683
+ ---
684
+
685
+ ## Architecture Reference
686
+
687
+ ### Encryption
688
+
689
+ | Message Type | Algorithm | Key Source |
690
+ |---|---|---|
691
+ | DM | NaCl box (X25519-XSalsa20-Poly1305) | Diffie-Hellman shared secret |
692
+ | Public channel | NaCl secretbox (XSalsa20-Poly1305) | Server key (from ServerMember.keyWraps) |
693
+ | Private channel | NaCl secretbox (XSalsa20-Poly1305) | Channel key (from ChannelMember.keyWraps) |
694
+
695
+ ### Roles
696
+
697
+ | Value | Role | Can do |
698
+ |---|---|---|
699
+ | 0 | Member | Send messages |
700
+ | 1 | Admin | Invite/remove members |
701
+ | 2 | Owner | Everything + cannot be removed |
702
+
703
+ ### Events (on-chain)
704
+
705
+ Messages are emitted as events (not stored in accounts):
706
+
707
+ ```typescript
708
+ // DM event
709
+ { sender, recipient, ciphertext, nonce, timestamp }
710
+
711
+ // Channel message event
712
+ { sender, channelId, version, keyVersion, ciphertext, nonce, timestamp }
713
+ ```
714
+
715
+ ### Security model
716
+
717
+ - **Forward secrecy**: Key rotation on removal AND voluntary leave
718
+ - **Full history**: New members get all historical keys
719
+ - **Public channel auth**: Sender's ServerMember PDA verified on-chain
720
+ - **Server channel auth**: Creator's ServerMember PDA required for channel creation
721
+ - **Admin verification**: PDA derivation check prevents spoofing
722
+ - **On-chain immutability**: Messages are permanent events — protocol cannot enforce deletion
723
+ - **Custody separation**: Signing key (A) ≠ encryption key (B) — custodial wallets can't read messages
724
+
725
+ ### Costs
726
+
727
+ | Action | Cost |
728
+ |--------|------|
729
+ | Send DM | ~5000 lamports + protocol fee + recipient min_fee |
730
+ | Send channel message | ~5000 lamports + protocol fee |
731
+ | Register | ~0.001 SOL rent (reclaimable) |
732
+ | Create server | ~0.001 SOL rent |
733
+ | Create channel | ~0.06 SOL rent (reclaimable) |
734
+ | ServerMember | ~0.002 SOL rent (reclaimable) |
735
+ | ChannelMember | ~0.002 SOL rent (reclaimable) |
736
+
737
+ All rent is reclaimable when accounts are closed.
738
+
739
+ ### Sponsored transactions (payer option)
740
+
741
+ Every transaction-sending method accepts an optional `payer` address via `TxOptions` (or inline `payer` field for params-style methods). When provided, the payer account pays the transaction fee instead of the signer.
742
+
743
+ ```typescript
744
+ // Server methods use options object
745
+ await messenger.createServer("My Server", { payer: "SponsorAddress" });
746
+ await messenger.send("Recipient", "Hello", undefined, { payer: "SponsorAddress" });
747
+
748
+ // Params-style methods use inline payer field
749
+ await messenger.sendChannelMessage({ channelPda, message: "Hi", channelKey, keyVersion: 0, payer: "SponsorAddress" });
750
+ await messenger.createChannel({ members: [myAddr, friend], payer: "SponsorAddress" });
751
+ ```
752
+
753
+ The payer must sign the transaction separately (e.g., via a relayer service or multi-sig flow). This enables gasless/sponsored experiences where users don't need SOL.