solana-messenger-sdk 1.2.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/SPEC.md +324 -0
- package/idl/messenger.json +1491 -0
- package/package.json +3 -1
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
|