solana-messenger-sdk 1.1.0 → 1.2.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/README.md CHANGED
@@ -5,8 +5,7 @@
5
5
 
6
6
  TypeScript SDK for **solana-messenger** — encrypted on-chain messaging with servers, channels, and DMs.
7
7
 
8
- **Program (mainnet):** `msg1jhfewu1hGDnQKGhXDmqas6JZTq7Lg7PbSX5jY9y`
9
- **Program (devnet):** `E6p87FCXNZFii8GJs5NY5xUmH8wKRPvGXn3Z2VhNqSv`
8
+ **Program (mainnet & devnet):** `msg1SxLsvf1ZL374noHwUWcVYjPsNSNwKb3xphg6Lxf`
10
9
 
11
10
  ## Install
12
11
 
package/SPEC.md ADDED
@@ -0,0 +1,324 @@
1
+ # solana-messenger
2
+
3
+ Encrypted messaging protocol on Solana. Servers, channels, DMs — fully on-chain, no backend required. Just pubkeys and math.
4
+
5
+ **Program (mainnet & devnet):** `msg1SxLsvf1ZL374noHwUWcVYjPsNSNwKb3xphg6Lxf`
6
+
7
+ ## Architecture
8
+
9
+ ### Hierarchy
10
+
11
+ ```
12
+ Server (Discord workspace)
13
+ ├── Public Channels (visible to all server members)
14
+ ├── Private Channels (invite-only, own encryption key)
15
+ └── Server Members (with role + key wraps)
16
+
17
+ Standalone Channel (group DM, no server)
18
+ ├── Channel Members (with key wraps)
19
+ └── Messages
20
+
21
+ Direct Messages (wallet-to-wallet, permanent)
22
+ ```
23
+
24
+ ### Key Registry
25
+
26
+ Every user registers an encryption public key on-chain:
27
+
28
+ - **Identity wallet (A):** Signs transactions, pays fees. Can be custodial (Privy/Turnkey).
29
+ - **Encryption keypair (B):** Generated locally, never leaves the agent. Used for encrypt/decrypt.
30
+ - **Registry PDA:** `seeds = ["messenger", A]` stores B's public key + min_fee.
31
+
32
+ ### Encryption Model
33
+
34
+ **Direct Messages:** NaCl box (X25519-XSalsa20-Poly1305) with Diffie-Hellman shared secret between sender and recipient encryption keys.
35
+
36
+ **Server/Channel Messages:** NaCl secretbox (XSalsa20-Poly1305) with a shared symmetric key:
37
+
38
+ - **Public channels** use the **server key** — one key per server version, wrapped once per ServerMember.
39
+ - **Private channels** have their **own key** — independent of server key, wrapped per ChannelMember.
40
+ - **Key rotation** happens on member removal or voluntary leave (forward secrecy).
41
+ - **New members** receive wraps for all historical key versions (Slack-style full history).
42
+ - **Messages** are lightweight: `ciphertext + nonce + key_version`. No per-message key wraps.
43
+
44
+ ### Discovery (Zero Backend)
45
+
46
+ With just wallet key A + encryption key B, a client can surface everything:
47
+
48
+ | What | Method |
49
+ |---|---|
50
+ | My servers | `getProgramAccounts` filter `ServerMember.member == me` |
51
+ | My private channels | `getProgramAccounts` filter `ChannelMember.member == me` |
52
+ | Server's public channels | `getProgramAccounts` filter `Channel.server_id == X` |
53
+ | My DMs | `getSignaturesForAddress(myWallet)` on program |
54
+ | Current channel version | Highest `key_wraps.version` in my ChannelMember |
55
+
56
+ ### Payer Separation (Relay Support)
57
+
58
+ All instructions support a **separate payer** account, enabling relay/gasless architectures where a backend pays transaction costs on behalf of users.
59
+
60
+ ### Fee System
61
+
62
+ - **Protocol fee:** Global fee per message, set by platform authority. Goes to fee vault.
63
+ - **Recipient fee (min_fee):** Per-recipient fee for DMs, set by each user on their registry.
64
+
65
+ ## On-Chain Program (Anchor/Rust)
66
+
67
+ ### Instructions
68
+
69
+ #### Platform Config
70
+
71
+ ```rust
72
+ pub fn initialize_config(ctx, fee_vault: Pubkey, protocol_fee: u64)
73
+ pub fn update_config(ctx, fee_vault: Option<Pubkey>, protocol_fee: Option<u64>)
74
+ ```
75
+
76
+ #### Identity
77
+
78
+ ```rust
79
+ pub fn register(ctx, encryption_pubkey: Pubkey)
80
+ pub fn update_encryption_key(ctx, new_key: Pubkey)
81
+ pub fn set_min_fee(ctx, min_fee: u64)
82
+ pub fn deregister(ctx)
83
+ ```
84
+
85
+ #### Direct Messages
86
+
87
+ ```rust
88
+ pub fn send_message(ctx, recipient: Pubkey, ciphertext: Vec<u8>, nonce: [u8; 24])
89
+ ```
90
+
91
+ Fees: protocol fee → vault, recipient min_fee → recipient wallet.
92
+
93
+ #### Servers
94
+
95
+ ```rust
96
+ // Create server. Creator becomes owner with initial key wrap.
97
+ // remaining_accounts[0] = owner's ServerMember PDA
98
+ pub fn create_server(ctx, server_id: Pubkey, name: [u8; 32],
99
+ owner_key_wrap: Vec<u8>, owner_key_nonce: [u8; 24])
100
+
101
+ // Admin/owner invites a member, wrapping server keys for them.
102
+ // remaining_accounts[0] = new member's ServerMember PDA
103
+ pub fn invite_to_server(ctx, new_member: Pubkey,
104
+ key_wraps: Vec<Vec<u8>>, key_nonces: Vec<[u8; 24]>)
105
+
106
+ // Remove member + rotate server key.
107
+ // remaining_accounts: [removed ServerMember, ...remaining ServerMember PDAs]
108
+ pub fn remove_server_member(ctx, removed_member: Pubkey,
109
+ new_key_wraps: Vec<Vec<u8>>, new_key_nonces: Vec<[u8; 24]>)
110
+
111
+ // Leave voluntarily + rotate server key for remaining members.
112
+ // remaining_accounts: [my ServerMember, ...remaining ServerMember PDAs]
113
+ pub fn leave_server(ctx,
114
+ new_key_wraps: Vec<Vec<u8>>, new_key_nonces: Vec<[u8; 24]>)
115
+
116
+ pub fn update_server(ctx, name: [u8; 32])
117
+
118
+ // Close server + all ServerMembers via remaining_accounts
119
+ pub fn close_server(ctx, member_pubkeys: Vec<Pubkey>)
120
+ ```
121
+
122
+ #### Channels
123
+
124
+ ```rust
125
+ // Create channel (standalone or within a server).
126
+ // For server channels: remaining_accounts[0] = creator's ServerMember PDA (auth)
127
+ // Then ChannelMember PDAs for private channels.
128
+ pub fn create_channel(ctx, channel_id: Pubkey, server_id: Option<Pubkey>,
129
+ channel_type: u8, members: Vec<Pubkey>,
130
+ key_wraps: Vec<Vec<u8>>, key_nonces: Vec<[u8; 24]>)
131
+
132
+ // Send message. key_version = which key version encrypted this message.
133
+ // For public channels: remaining_accounts[0] = sender's ServerMember PDA
134
+ pub fn send_channel_message(ctx, ciphertext: Vec<u8>, nonce: [u8; 24], key_version: u32)
135
+
136
+ // Add members to private channel with all historical key versions.
137
+ pub fn add_channel_members(ctx, new_members: Vec<Pubkey>,
138
+ key_wraps: Vec<Vec<Vec<u8>>>, key_nonces: Vec<Vec<[u8; 24]>>)
139
+
140
+ // Remove members + rotate channel key (creates new Channel PDA at version+1).
141
+ pub fn remove_channel_members(ctx, remove_list: Vec<Pubkey>,
142
+ new_key_wraps: Vec<Vec<u8>>, new_key_nonces: Vec<[u8; 24]>)
143
+
144
+ pub fn close_channel(ctx)
145
+ ```
146
+
147
+ ### Accounts
148
+
149
+ ```rust
150
+ PlatformConfig // PDA: ["config"]
151
+ authority: Pubkey, fee_vault: Pubkey, protocol_fee: u64, updated_at: i64
152
+
153
+ EncryptionRegistry // PDA: ["messenger", owner]
154
+ owner: Pubkey, encryption_key: Pubkey, min_fee: u64, created_at: i64, updated_at: i64
155
+
156
+ Server // PDA: ["server", server_id]
157
+ server_id: Pubkey, owner: Pubkey, name: [u8; 32], created_at: i64, updated_at: i64
158
+
159
+ ServerMember // PDA: ["server_member", server_id, member]
160
+ server_id: Pubkey, member: Pubkey, role: u8, key_wraps: Vec<KeyWrap>, joined_at: i64
161
+
162
+ Channel // PDA: ["channel", channel_id, version_bytes]
163
+ channel_id: Pubkey, server_id: Option<Pubkey>, creator: Pubkey,
164
+ version: u32, channel_type: u8, members: Vec<Pubkey>, created_at: i64, updated_at: i64
165
+
166
+ ChannelMember // PDA: ["channel_member", channel_id, member]
167
+ channel_id: Pubkey, member: Pubkey, key_wraps: Vec<KeyWrap>, joined_at: i64
168
+
169
+ KeyWrap { version: u32, wrapped_key: Vec<u8>, nonce: [u8; 24] }
170
+ ```
171
+
172
+ ### Events
173
+
174
+ ```rust
175
+ MessageSent {
176
+ sender: Pubkey, recipient: Pubkey,
177
+ ciphertext: Vec<u8>, nonce: [u8; 24], timestamp: i64
178
+ }
179
+
180
+ ChannelMessageSent {
181
+ sender: Pubkey, channel_id: Pubkey,
182
+ version: u32, // channel version (PDA version)
183
+ key_version: u32, // which key version was used to encrypt
184
+ ciphertext: Vec<u8>, nonce: [u8; 24], timestamp: i64
185
+ }
186
+ ```
187
+
188
+ ### Roles
189
+
190
+ | Value | Role | Permissions |
191
+ |-------|------|-------------|
192
+ | 0 | Member | Send messages |
193
+ | 1 | Admin | Invite/remove members |
194
+ | 2 | Owner | Full control, cannot be removed |
195
+
196
+ ### Channel Types
197
+
198
+ | Value | Type | Key Source | Members Vec |
199
+ |-------|------|-----------|-------------|
200
+ | 0 | Private | Own channel key (ChannelMember wraps) | Populated |
201
+ | 1 | Public | Server key (ServerMember wraps) | Empty |
202
+
203
+ ### Errors
204
+
205
+ | Code | Name | Description |
206
+ |------|------|-------------|
207
+ | 6000 | MessageTooLarge | Ciphertext exceeds 900 bytes |
208
+ | 6001 | EmptyMessage | Ciphertext is empty |
209
+ | 6002 | InvalidFeeVault | Fee vault doesn't match platform config |
210
+ | 6003 | InvalidRecipient | Recipient wallet doesn't match registry owner |
211
+ | 6004 | TooManyMembers | Channel exceeds 256 members |
212
+ | 6005 | EmptyGroup | Must have at least one member |
213
+ | 6006 | CreatorNotInMembers | Creator must be in members list |
214
+ | 6007 | NotGroupMember | Sender is not a member |
215
+ | 6008 | KeyWrapMismatch | Key wraps count doesn't match |
216
+ | 6009 | MemberRecordMismatch | Member record count mismatch |
217
+ | 6010 | InvalidMemberRecord | Invalid member record PDA |
218
+ | 6011 | AlreadyMember | Already a member |
219
+ | 6012 | Unauthorized | Insufficient permissions |
220
+ | 6013 | CannotRemoveOwner | Cannot remove server owner |
221
+ | 6014 | PublicRequiresServer | Public channels require a server |
222
+ | 6015 | PublicNoMembers | Public channels don't use member lists |
223
+
224
+ ### Security Model
225
+
226
+ - **Public channel sender verification:** `send_channel_message` requires sender's ServerMember PDA for public channels.
227
+ - **Server channel creation:** `create_channel` requires creator's ServerMember PDA when `server_id` is set.
228
+ - **Admin verification:** `remove_server_member` verifies admin's ServerMember PDA via derivation.
229
+ - **Key rotation on removal AND leave:** Both `remove_server_member` and `leave_server` rotate the server key.
230
+ - **Forward secrecy:** After key rotation, removed members cannot decrypt new messages.
231
+ - **Full history access:** New members receive all historical key wraps — past messages are readable.
232
+ - **On-chain immutability:** Messages are permanent events. Protocol cannot enforce retroactive deletion.
233
+
234
+ ## TypeScript SDK (`solana-messenger-sdk`)
235
+
236
+ Pure `@solana/kit` v2 — no Anchor client-side dependency.
237
+
238
+ ### Setup
239
+
240
+ ```typescript
241
+ import { SolanaMessenger } from "solana-messenger-sdk";
242
+
243
+ const messenger = new SolanaMessenger({
244
+ apiKey: process.env.HELIUS_API_KEY!,
245
+ keypair: keypairBytes,
246
+ });
247
+
248
+ await messenger.init();
249
+ ```
250
+
251
+ ### Direct Messages
252
+
253
+ ```typescript
254
+ await messenger.send(recipientAddress, "Hello!");
255
+ const messages = await messenger.read({ limit: 10 });
256
+ const unsub = await messenger.listen((msg) => console.log(msg.text));
257
+ ```
258
+
259
+ ### Servers
260
+
261
+ ```typescript
262
+ // Create server (you become owner)
263
+ const { serverId, serverPda, signature } = await messenger.createServer("My Server");
264
+
265
+ // Invite member (admin wraps keys for them)
266
+ await messenger.inviteToServer(serverId, memberAddress, [serverKey]);
267
+
268
+ // Leave server (rotates key for remaining members)
269
+ await messenger.leaveServer(serverId, [remainingMember1, remainingMember2]);
270
+ ```
271
+
272
+ ### Channels
273
+
274
+ ```typescript
275
+ // Private channel (standalone group DM)
276
+ const { channelId, channelPda } = await messenger.createChannel({
277
+ members: [myAddress, friend1, friend2],
278
+ });
279
+
280
+ // Public channel in a server
281
+ const { channelId: pubId } = await messenger.createChannel({
282
+ serverId,
283
+ channelType: ChannelType.Public,
284
+ members: [],
285
+ });
286
+
287
+ // Send message
288
+ await messenger.sendChannelMessage({
289
+ channelPda,
290
+ message: "Hello team!",
291
+ channelKey,
292
+ keyVersion: 0,
293
+ senderServerMemberPda, // required for public channels
294
+ });
295
+
296
+ // Add/remove members (creator only)
297
+ await messenger.addChannelMembers(channelPda, channelId, [newMember], [allKeyVersions]);
298
+ const { newChannelPda } = await messenger.removeChannelMembers(
299
+ channelId, currentVersion, [removedMember], [remainingMembers]
300
+ );
301
+ ```
302
+
303
+ ### Identity
304
+
305
+ ```typescript
306
+ await messenger.register(encryptionPubkey);
307
+ await messenger.setMinFee(10000); // lamports to message me
308
+ const encKey = await messenger.lookupEncryptionKey(walletAddress);
309
+ ```
310
+
311
+ ## Cost
312
+
313
+ - **Send DM:** ~5000 lamports tx fee + protocol fee + recipient min_fee
314
+ - **Send channel message:** ~5000 lamports tx fee + protocol fee
315
+ - **Register:** ~0.001 SOL rent
316
+ - **Create server:** ~0.001 SOL rent (Server) + ~0.002 SOL rent (ServerMember)
317
+ - **Create channel:** ~0.06 SOL rent (Channel, max member allocation)
318
+ - **All rent is reclaimable on close.**
319
+
320
+ ## Dependencies
321
+
322
+ - `@solana/kit` — Solana web3 v2
323
+ - `tweetnacl` — NaCl encryption
324
+ - `helius-sdk` — Helius RPC + enhanced APIs
@@ -39,7 +39,7 @@ const SEND_CHANNEL_MESSAGE_DISC = new Uint8Array([207, 181, 41, 74, 206, 175, 63
39
39
  const ADD_CHANNEL_MEMBERS_DISC = new Uint8Array([93, 252, 61, 110, 120, 252, 239, 227]);
40
40
  const REMOVE_CHANNEL_MEMBERS_DISC = new Uint8Array([77, 154, 200, 78, 112, 183, 64, 253]);
41
41
  const CLOSE_CHANNEL_DISC = new Uint8Array([0, 104, 36, 1, 66, 0, 103, 157]);
42
- const DEFAULT_PROGRAM_ID = "msg1jhfewu1hGDnQKGhXDmqas6JZTq7Lg7PbSX5jY9y";
42
+ const DEFAULT_PROGRAM_ID = "msg1SxLsvf1ZL374noHwUWcVYjPsNSNwKb3xphg6Lxf";
43
43
  const SYSTEM_PROGRAM = "11111111111111111111111111111111";
44
44
  // === Platform Config ===
45
45
  function buildInitializeConfigInstruction(params) {
package/dist/messenger.js CHANGED
@@ -17,7 +17,7 @@ const registry_1 = require("./registry");
17
17
  const keys_1 = require("./keys");
18
18
  const os_1 = require("os");
19
19
  const path_1 = require("path");
20
- const DEFAULT_PROGRAM_ID = "msg1jhfewu1hGDnQKGhXDmqas6JZTq7Lg7PbSX5jY9y";
20
+ const DEFAULT_PROGRAM_ID = "msg1SxLsvf1ZL374noHwUWcVYjPsNSNwKb3xphg6Lxf";
21
21
  function isSelfCustody(config) {
22
22
  return "keypair" in config;
23
23
  }
package/dist/pda.js CHANGED
@@ -8,7 +8,7 @@ exports.deriveChannelPda = deriveChannelPda;
8
8
  exports.deriveChannelMemberPda = deriveChannelMemberPda;
9
9
  const kit_1 = require("@solana/kit");
10
10
  const utils_1 = require("./utils");
11
- const DEFAULT_PROGRAM_ID = "msg1jhfewu1hGDnQKGhXDmqas6JZTq7Lg7PbSX5jY9y";
11
+ const DEFAULT_PROGRAM_ID = "msg1SxLsvf1ZL374noHwUWcVYjPsNSNwKb3xphg6Lxf";
12
12
  async function deriveConfigPda(programId) {
13
13
  const [pda] = await (0, kit_1.getProgramDerivedAddress)({
14
14
  programAddress: (0, kit_1.address)(programId ?? DEFAULT_PROGRAM_ID),