@dimcool/sdk 0.1.0-beta.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 ADDED
@@ -0,0 +1,3028 @@
1
+ # Dim SDK
2
+
3
+ A TypeScript SDK for interacting with the Dim API. The SDK provides a clean, type-safe interface organized by feature modules.
4
+
5
+ Canonical docs:
6
+
7
+ - https://docs.dim.cool/capabilities/api-admin-operations
8
+ - https://docs.dim.cool/quickstart/sdk-node
9
+ - https://docs.dim.cool/api-reference/overview
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @dimcool/sdk
15
+ # or
16
+ bun add @dimcool/sdk
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### Browser
22
+
23
+ ```typescript
24
+ import { SDK, BrowserLocalStorage } from '@dimcool/sdk';
25
+
26
+ const sdk = new SDK({
27
+ appId: 'dim',
28
+ baseUrl: 'https://api.example.com',
29
+ storage: new BrowserLocalStorage(),
30
+ });
31
+
32
+ // Authenticate
33
+ await sdk.auth.login('user@example.com', 'password');
34
+
35
+ // Check authentication status
36
+ if (sdk.auth.isAuthenticated()) {
37
+ // Use SDK methods
38
+ const users = await sdk.users.getUsers();
39
+ const flags = await sdk.featureFlags.getFeatureFlags();
40
+ }
41
+ ```
42
+
43
+ ### Node.js
44
+
45
+ ```typescript
46
+ import { SDK, NodeStorage } from '@dimcool/sdk';
47
+
48
+ const sdk = new SDK({
49
+ appId: 'dim-bots',
50
+ baseUrl: 'https://api.dim.cool',
51
+ storage: new NodeStorage(),
52
+ });
53
+
54
+ await sdk.auth.login('user@example.com', 'password');
55
+ ```
56
+
57
+ ### Agent Usage (Wallet Auth)
58
+
59
+ AI agents authenticate using a Solana keypair — no email or browser needed. Use `appId: 'dim-agents'` to identify agent traffic.
60
+
61
+ ```typescript
62
+ import { SDK, NodeStorage } from '@dimcool/sdk';
63
+ import { Keypair } from '@solana/web3.js';
64
+ import bs58 from 'bs58';
65
+ import nacl from 'tweetnacl';
66
+
67
+ const sdk = new SDK({
68
+ appId: 'dim-agents',
69
+ baseUrl: 'https://api.dim.cool',
70
+ storage: new NodeStorage(),
71
+ });
72
+
73
+ // Wallet auth
74
+ const keypair = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY!));
75
+ const walletAddress = keypair.publicKey.toBase58();
76
+ sdk.wallet.setSigner({
77
+ address: walletAddress,
78
+ signMessage: async (message: string) => {
79
+ const messageBytes = new TextEncoder().encode(message);
80
+ const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
81
+ return Buffer.from(signature).toString('base64');
82
+ },
83
+ signTransaction: async (transaction) => {
84
+ transaction.partialSign(keypair);
85
+ return transaction;
86
+ },
87
+ });
88
+
89
+ const { access_token, user } = await sdk.auth.loginWithWallet({
90
+ referralCode: 'optional-referral-code',
91
+ walletMeta: { type: 'keypair' },
92
+ });
93
+
94
+ // WebSocket (for games and chat)
95
+ sdk.wsTransport.setAccessToken(access_token);
96
+ await sdk.ensureWebSocketConnected(10000);
97
+
98
+ // Now use any SDK module
99
+ const balance = await sdk.wallet.getBalances();
100
+ const referrals = await sdk.referrals.getSummary();
101
+ ```
102
+
103
+ **MCP alternative:** For MCP-compatible agents (Claude, Cursor, OpenClaw), use [`@dimcool/mcp`](../dim-mcp/README.md) instead — it wraps this SDK into MCP tools automatically.
104
+
105
+ ## Configuration
106
+
107
+ The SDK constructor accepts a configuration object:
108
+
109
+ ```typescript
110
+ interface SDKConfig {
111
+ appId: string; // Required: identifies the app (e.g. 'dim', 'dim-admin')
112
+ baseUrl?: string; // Default: 'http://localhost:3000'
113
+ storage: IStorage; // Required: Storage implementation
114
+ httpClient?: IHttpClient; // Optional: injectable HTTP client for testing
115
+ wsTransport?: WsTransport; // Optional: injectable WS transport
116
+ logger?: ILogger; // Optional: logger instance
117
+ }
118
+ ```
119
+
120
+ ### appId
121
+
122
+ Every SDK instance must provide an `appId` that identifies which app is making requests. The API validates this against its [app registry](../../apps/api/src/config/apps.config.ts) and rejects unknown app IDs.
123
+
124
+ The `appId` is:
125
+
126
+ - Sent as `X-App-Id` header on all HTTP requests
127
+ - Included in the Socket.IO auth handshake for WebSocket connections
128
+ - Stored on user records (`signupAppId`, `lastLoginAppId`) and sessions
129
+
130
+ ## Storage
131
+
132
+ The SDK requires a storage implementation to persist authentication tokens. Two implementations are provided:
133
+
134
+ - **`BrowserLocalStorage`**: Uses browser's `localStorage` API (browser only)
135
+ - **`NodeStorage`**: Uses in-memory storage for Node.js environments
136
+
137
+ ### Custom Storage
138
+
139
+ You can implement your own storage by creating a class that implements the `IStorage` interface:
140
+
141
+ ```typescript
142
+ import { IStorage } from '@dimcool/sdk';
143
+
144
+ class CustomStorage implements IStorage {
145
+ set(key: string, value: string): void {
146
+ // Your implementation
147
+ }
148
+
149
+ get(key: string): string | null {
150
+ // Your implementation
151
+ }
152
+
153
+ delete(key: string): void {
154
+ // Your implementation
155
+ }
156
+ }
157
+ ```
158
+
159
+ ## API Reference
160
+
161
+ The SDK is organized into modules accessible via properties on the SDK instance:
162
+
163
+ - `sdk.auth` - Authentication methods
164
+ - `sdk.admin` - Platform-wide admin operations
165
+ - `sdk.users` - User management and social features
166
+ - `sdk.featureFlags` - Feature flag management
167
+ - `sdk.lobbies` - Game lobby management
168
+ - `sdk.games` - Game type management
169
+ - `sdk.chat` - Generic chat system (lobbies, games, DMs)
170
+ - `sdk.challenges` - Create and accept game challenges (standalone; global chat `/challenge` is a shorthand)
171
+ - `sdk.notifications` - App notifications (admin only)
172
+ - `sdk.wallet` - Solana wallet management and transaction signing
173
+ - `sdk.escrow` - Escrow and deposit management for game lobbies
174
+ - `sdk.activity` - Global activity feed (signups, wins, lobbies, games)
175
+ - `sdk.leaderboards` - Leaderboard data (global, per-game, friends)
176
+ - `sdk.reports` - User reports (create reports, admin management)
177
+ - `sdk.support` - Support tickets (create tickets, messages, track issues)
178
+ - `sdk.markets` - Prediction market trading (buy/sell shares, positions, redemption)
179
+
180
+ ---
181
+
182
+ ## Authentication (`sdk.auth`)
183
+
184
+ ### `login(email: string, password: string): Promise<LoginResponse>`
185
+
186
+ Authenticate a user and store the access token.
187
+
188
+ ```typescript
189
+ const response = await sdk.auth.login('user@example.com', 'password');
190
+ // response: { access_token: string, user: { id, email, username, isAdmin, chessElo, points } }
191
+ ```
192
+
193
+ ### `logout(): void`
194
+
195
+ Clear the authentication token and log out the user.
196
+
197
+ ```typescript
198
+ sdk.auth.logout();
199
+ ```
200
+
201
+ ### `isAuthenticated(): boolean`
202
+
203
+ Check if the user is currently authenticated.
204
+
205
+ ```typescript
206
+ if (sdk.auth.isAuthenticated()) {
207
+ // User is logged in
208
+ }
209
+ ```
210
+
211
+ ### `loginWithWallet(options?: { referralCode?: string; walletMeta?: WalletMeta }): Promise<LoginResponse>`
212
+
213
+ Authenticate with a configured wallet signer. The SDK handles handshake generation and message-signature verification flow internally.
214
+
215
+ ```typescript
216
+ const response = await sdk.auth.loginWithWallet({
217
+ referralCode: 'optional-referral-code',
218
+ walletMeta: { type: 'keypair' },
219
+ });
220
+ // response: { access_token: string, user: { id, email, username, isAdmin, chessElo, points } }
221
+ ```
222
+
223
+ **Complete Wallet Login Flow:**
224
+
225
+ ```typescript
226
+ import { SDK, BrowserLocalStorage } from '@dimcool/sdk';
227
+ import { Keypair } from '@solana/web3.js';
228
+ import * as nacl from 'tweetnacl';
229
+
230
+ const sdk = new SDK({
231
+ appId: 'dim',
232
+ baseUrl: 'https://api.example.com',
233
+ storage: new BrowserLocalStorage(),
234
+ });
235
+
236
+ // 1. Get the user's wallet address (from their wallet provider)
237
+ const walletAddress = '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU';
238
+
239
+ // 2. Register signer (SDK handles handshake + signing orchestration)
240
+ sdk.wallet.setSigner({
241
+ address: walletAddress,
242
+ signMessage: async (message: string) => {
243
+ const messageBytes = new TextEncoder().encode(message);
244
+ const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
245
+ return Buffer.from(signature).toString('base64');
246
+ },
247
+ signTransaction: async (transaction) => transaction,
248
+ });
249
+
250
+ // 3. Login with wallet
251
+ const response = await sdk.auth.loginWithWallet();
252
+ console.log('Logged in as:', response.user.id);
253
+ ```
254
+
255
+ **Note:** In a real application, signer methods are typically provided by wallet providers (Phantom, Solflare, Keypair, etc.). `sdk.auth.loginWithWallet` handles the backend auth flow once signer is configured.
256
+
257
+ ---
258
+
259
+ ## Users (`sdk.users`)
260
+
261
+ ### Admin Methods
262
+
263
+ #### `getUsers(page?: number, limit?: number, username?: string): Promise<PaginatedUsers>`
264
+
265
+ Get a paginated list of users. Admin only. Optionally filter by username prefix.
266
+
267
+ ```typescript
268
+ const result = await sdk.users.getUsers(1, 10, 'john');
269
+ // result: { users: User[], total: number, page: number, limit: number, totalPages: number }
270
+ ```
271
+
272
+ #### `getUserById(id: string): Promise<User>`
273
+
274
+ Get user details by ID.
275
+
276
+ ```typescript
277
+ const user = await sdk.users.getUserById('user-id');
278
+ ```
279
+
280
+ ### Username Management
281
+
282
+ #### `isUsernameAvailable(username: string): Promise<UsernameAvailabilityResponse>`
283
+
284
+ Check if a username is valid and available.
285
+
286
+ ```typescript
287
+ const result = await sdk.users.isUsernameAvailable('myusername');
288
+ // result: { valid: boolean, available: boolean }
289
+ ```
290
+
291
+ #### `updateUsername(username: string): Promise<User>`
292
+
293
+ Update the current user's username. Username must be:
294
+
295
+ - Alphanumeric only
296
+ - 3-20 characters
297
+ - Unique
298
+
299
+ ```typescript
300
+ const availability = await sdk.users.isUsernameAvailable('myusername');
301
+ if (availability.valid && availability.available) {
302
+ const updatedUser = await sdk.users.updateUsername('myusername');
303
+ }
304
+ ```
305
+
306
+ #### `getPublicUserByUsername(username: string): Promise<PublicUser>`
307
+
308
+ Get public user profile by username. Returns user info with `friendsCount`, `chessElo`, `points`, and optional `isFriend` (if authenticated).
309
+
310
+ ```typescript
311
+ const publicUser = await sdk.users.getPublicUserByUsername('someuser');
312
+ console.log(`Friends count: ${publicUser.friendsCount}`);
313
+ console.log(`Is friend: ${publicUser.isFriend}`);
314
+ ```
315
+
316
+ #### `searchUsers(username: string, page?: number, limit?: number): Promise<PaginatedSearchUsers>`
317
+
318
+ Search users by username prefix.
319
+
320
+ ```typescript
321
+ const results = await sdk.users.searchUsers('john', 1, 10);
322
+ // results: { users: SearchUser[], total: number, page: number, limit: number, totalPages: number }
323
+ ```
324
+
325
+ ### Friends
326
+
327
+ #### `getFriends(userId: string, page?: number, limit?: number, search?: string): Promise<PaginatedFriends>`
328
+
329
+ Get paginated friends list for a user. Requires authentication. Users can only view their own friends; admins can view any user's friends.
330
+
331
+ ```typescript
332
+ const friends = await sdk.users.getFriends('user-id', 1, 10, 'search-term');
333
+ // friends: { friends: PublicUser[], total: number, page: number, limit: number, totalPages: number }
334
+ ```
335
+
336
+ #### `addFriend(userId: string): Promise<{ message: string }>`
337
+
338
+ Add a user as a friend.
339
+
340
+ ```typescript
341
+ await sdk.users.addFriend('user-id');
342
+ ```
343
+
344
+ #### `removeFriend(userId: string): Promise<{ message: string }>`
345
+
346
+ Remove a user from friends.
347
+
348
+ ```typescript
349
+ await sdk.users.removeFriend('user-id');
350
+ ```
351
+
352
+ #### `isFriend(userId: string): Promise<boolean>`
353
+
354
+ Check if a user is a friend. Note: This is a helper method that may require additional API calls. For better performance, use `getPublicUserByUsername()` which includes `isFriend` in the response.
355
+
356
+ ```typescript
357
+ const isFriend = await sdk.users.isFriend('user-id');
358
+ ```
359
+
360
+ ### Profile Management
361
+
362
+ #### `updateProfile(data: { name?: string, bio?: string, username?: string }): Promise<User>`
363
+
364
+ Update the current user's profile. All fields are optional.
365
+
366
+ ```typescript
367
+ const updatedUser = await sdk.users.updateProfile({
368
+ name: 'John Doe',
369
+ bio: 'Gaming enthusiast',
370
+ username: 'johndoe',
371
+ });
372
+ ```
373
+
374
+ #### `uploadAvatar(file: File): Promise<User>`
375
+
376
+ Upload an avatar image for the current user. The file must be:
377
+
378
+ - An image (JPEG, PNG, WebP, or GIF)
379
+ - Maximum 5MB in size
380
+
381
+ ```typescript
382
+ const fileInput = document.querySelector('input[type="file"]');
383
+ const file = fileInput.files[0];
384
+ const updatedUser = await sdk.users.uploadAvatar(file);
385
+ ```
386
+
387
+ #### `uploadCoverImage(file: File): Promise<User>`
388
+
389
+ Upload a cover image for the current user. The file must be:
390
+
391
+ - An image (JPEG, PNG, WebP, or GIF)
392
+ - Maximum 5MB in size
393
+
394
+ ```typescript
395
+ const fileInput = document.querySelector('input[type="file"]');
396
+ const file = fileInput.files[0];
397
+ const updatedUser = await sdk.users.uploadCoverImage(file);
398
+ ```
399
+
400
+ #### `removeAvatar(): Promise<User>`
401
+
402
+ Remove the current user's avatar.
403
+
404
+ ```typescript
405
+ const updatedUser = await sdk.users.removeAvatar();
406
+ ```
407
+
408
+ #### `removeCoverImage(): Promise<User>`
409
+
410
+ Remove the current user's cover image.
411
+
412
+ ```typescript
413
+ const updatedUser = await sdk.users.removeCoverImage();
414
+ ```
415
+
416
+ #### `getCurrentGame(userId: string): Promise<CurrentGame | null>`
417
+
418
+ Get the current active game for a user (if they are a player in one). Returns `{ gameId, gameType, status }` or `null`. Used for profile "Playing X" banner and spectate link.
419
+
420
+ ```typescript
421
+ const current = await sdk.users.getCurrentGame('user-id');
422
+ if (current) {
423
+ console.log(`User is in game ${current.gameId} (${current.gameType})`);
424
+ }
425
+ ```
426
+
427
+ #### `getUserActivity(userId: string): Promise<UserActivity>`
428
+
429
+ Get a user's current activity for the **spectate-user** flow: whether they are in a game, in a lobby, or idle. Use this when building a "Watch [username]" experience: poll every few seconds while showing a waiting room; when `status === 'in_game'`, redirect to the game with `gameType` and `gameId`.
430
+
431
+ ```typescript
432
+ const activity = await sdk.users.getUserActivity('user-id');
433
+ // activity: { status: 'in_game' | 'in_lobby' | 'idle', gameId?, gameType?, lobbyId?, lobbyStatus? }
434
+ if (activity.status === 'in_game' && activity.gameId && activity.gameType) {
435
+ // Navigate to /game/${activity.gameType}/${activity.gameId}
436
+ }
437
+ ```
438
+
439
+ - **Resolving username to userId:** call `getPublicUserByUsername(username)` first to get the user's `id`, then call `getUserActivity(id)`.
440
+ - **Polling:** when showing a waiting room (user idle or in lobby), poll every 3–5 seconds until they enter a game or the viewer leaves.
441
+ - For listing and joining live games by game ID, see **Games** → `getLiveGames()` and **Spectating** in the docs.
442
+
443
+ #### `getGameHistory(userId: string): Promise<GameHistoryItem[]>`
444
+
445
+ Get game history for a user. Returns the last 10 games played by the specified user. Any logged-in user can query game history for any player.
446
+
447
+ ```typescript
448
+ const history = await sdk.users.getGameHistory('user-id');
449
+ // history: GameHistoryItem[]
450
+ // Each item includes: id, gameType, gameName, gameImage, result, betAmount, winnings, opponent, playedAt
451
+ ```
452
+
453
+ **Response format:**
454
+
455
+ ```typescript
456
+ interface GameHistoryItem {
457
+ id: string;
458
+ gameType: string;
459
+ gameName: string;
460
+ gameImage?: string;
461
+ result: 'win' | 'loss' | 'draw';
462
+ betAmount: number;
463
+ winnings?: number;
464
+ opponent?: {
465
+ id: string;
466
+ username?: string;
467
+ avatar?: string;
468
+ };
469
+ playedAt: string;
470
+ }
471
+ ```
472
+
473
+ #### `getUserStats(userId: string): Promise<UserStats>`
474
+
475
+ Get aggregated game statistics for a user. Returns aggregated stats including games played, wins, losses, total earned, and total lost. Any logged-in user can query stats for any player.
476
+
477
+ ```typescript
478
+ const stats = await sdk.users.getUserStats('user-id');
479
+ // stats: UserStats
480
+ // Includes: gamesPlayed, wins, losses, totalEarned, totalLost, totalFeesPaid, referralEarned, chessElo, points
481
+ ```
482
+
483
+ **Response format:**
484
+
485
+ ```typescript
486
+ interface UserStats {
487
+ gamesPlayed: number; // Total count of all games where user is in playerIds
488
+ wins: number; // Count of games where user won
489
+ losses: number; // Count of games where user lost
490
+ totalEarned: number; // Sum of wonAmount for all games won
491
+ referralEarned: number; // Sum of referral rewards
492
+ totalLost: number; // Sum of betAmount for all games lost
493
+ totalFeesPaid: number; // Sum of platform fees paid
494
+ chessElo: number; // Current chess Elo rating
495
+ points: number; // Accumulated engagement points (10 pts per $0.01 of fee)
496
+ }
497
+ ```
498
+
499
+ **Example:**
500
+
501
+ ```typescript
502
+ const stats = await sdk.users.getUserStats('user-id');
503
+ console.log(`Games played: ${stats.gamesPlayed}`);
504
+ console.log(`Wins: ${stats.wins}, Losses: ${stats.losses}`);
505
+ console.log(`Total earned: $${stats.totalEarned}`);
506
+ console.log(`Total lost: $${stats.totalLost}`);
507
+ console.log(`Chess Elo: ${stats.chessElo}`);
508
+ console.log(`Points: ${stats.points}`);
509
+ ```
510
+
511
+ **Notes:**
512
+
513
+ - Draws (`winnerId === null`) are counted in `gamesPlayed` but not in `wins`, `losses`, `totalEarned`, or `totalLost`
514
+ - Returns all zeros if the user has no game history
515
+ - `totalEarned` and `totalLost` default to 0 if `wonAmount` or `betAmount` are null
516
+
517
+ ## Admin (`sdk.admin`)
518
+
519
+ ### `getStats(): Promise<AdminStats>`
520
+
521
+ Get platform-wide aggregated statistics. Admin only. Returns total games played, total fees generated, and total users.
522
+
523
+ ```typescript
524
+ const stats = await sdk.admin.getStats();
525
+ // stats: AdminStats
526
+ // Includes: totalGamesPlayed, totalFeesGenerated, totalUsers
527
+ ```
528
+
529
+ **Response format:**
530
+
531
+ ```typescript
532
+ interface AdminStats {
533
+ totalGamesPlayed: number; // Total count of all games in GameHistory
534
+ totalFeesGenerated: number; // Sum of 1% of betAmount for all games (where betAmount is not null)
535
+ totalUsers: number; // Total count of users in User table
536
+ }
537
+ ```
538
+
539
+ **Example:**
540
+
541
+ ```typescript
542
+ const stats = await sdk.admin.getStats();
543
+ console.log(`Total games: ${stats.totalGamesPlayed}`);
544
+ console.log(`Total fees: $${stats.totalFeesGenerated.toFixed(2)}`);
545
+ console.log(`Total users: ${stats.totalUsers}`);
546
+ ```
547
+
548
+ **Notes:**
549
+
550
+ - **Admin only**: Requires admin authentication. Non-admin users will receive a 403 Forbidden error
551
+
552
+ ### User Banning (Admin)
553
+
554
+ ### `banUser(userId: string, reason?: string): Promise<User>`
555
+
556
+ Ban a user (admin only). Banned users cannot log in or access the API.
557
+
558
+ ```typescript
559
+ const user = await sdk.admin.banUser(
560
+ 'user-id',
561
+ 'Violation of terms of service',
562
+ );
563
+ console.log(`User banned: ${user.banned}`);
564
+ ```
565
+
566
+ **Parameters:**
567
+
568
+ - `userId: string` - The ID of the user to ban
569
+ - `reason?: string` - Optional reason for the ban
570
+
571
+ **Returns:** Updated user object with `banned: true`, `bannedAt`, and `bannedReason` fields.
572
+
573
+ ### `unbanUser(userId: string): Promise<User>`
574
+
575
+ Unban a user (admin only). Restores the user's access to the platform.
576
+
577
+ ```typescript
578
+ const user = await sdk.admin.unbanUser('user-id');
579
+ console.log(`User unbanned: ${!user.banned}`);
580
+ ```
581
+
582
+ **Parameters:**
583
+
584
+ - `userId: string` - The ID of the user to unban
585
+
586
+ **Returns:** Updated user object with `banned: false`.
587
+
588
+ **Notes:**
589
+
590
+ - Banned users receive a 403 Forbidden error with code `ACCOUNT_BANNED` when trying to log in or access any authenticated endpoint
591
+ - Banned users are directed to contact support@dim.gg to appeal their ban
592
+
593
+ ---
594
+
595
+ ## Reports (`sdk.reports`)
596
+
597
+ The reports module allows users to report other users and admins to manage reports.
598
+
599
+ ### User Methods
600
+
601
+ #### `create(reportedUserId: string, reason: string): Promise<Report>`
602
+
603
+ Create a report against another user. Rate limited to 5 reports per hour.
604
+
605
+ ```typescript
606
+ const report = await sdk.reports.create(
607
+ 'user-id',
608
+ 'Inappropriate behavior in chat',
609
+ );
610
+ console.log(`Report created: ${report.id}`);
611
+ ```
612
+
613
+ **Parameters:**
614
+
615
+ - `reportedUserId: string` - The ID of the user being reported
616
+ - `reason: string` - The reason for the report (max 300 characters)
617
+
618
+ **Notes:**
619
+
620
+ - Users cannot report themselves
621
+ - Rate limited to 5 reports per hour per user
622
+ - Returns 429 Too Many Requests when rate limit is exceeded
623
+
624
+ ### Admin Methods
625
+
626
+ #### `list(options?: GetReportsOptions): Promise<PaginatedReports>`
627
+
628
+ Get all reports (admin only, paginated).
629
+
630
+ ```typescript
631
+ const reports = await sdk.reports.list({
632
+ page: 1,
633
+ limit: 10,
634
+ status: 'PENDING',
635
+ });
636
+ console.log(`Found ${reports.total} reports`);
637
+ ```
638
+
639
+ **Parameters:**
640
+
641
+ - `options.page?: number` - Page number (default: 1)
642
+ - `options.limit?: number` - Results per page (default: 10)
643
+ - `options.status?: ReportStatus` - Filter by status (PENDING, READ, RESOLVED, DISMISSED)
644
+
645
+ #### `getById(id: string): Promise<Report>`
646
+
647
+ Get a single report by ID (admin only).
648
+
649
+ ```typescript
650
+ const report = await sdk.reports.getById('report-id');
651
+ ```
652
+
653
+ #### `getByUser(userId: string): Promise<Report[]>`
654
+
655
+ Get all reports for a specific user (admin only).
656
+
657
+ ```typescript
658
+ const reports = await sdk.reports.getByUser('user-id');
659
+ console.log(`User has ${reports.length} reports`);
660
+ ```
661
+
662
+ #### `getCountByUser(userId: string): Promise<ReportCount>`
663
+
664
+ Get report count for a specific user (admin only).
665
+
666
+ ```typescript
667
+ const count = await sdk.reports.getCountByUser('user-id');
668
+ console.log(`User has ${count.count} reports`);
669
+ ```
670
+
671
+ #### `getPendingCount(): Promise<{ count: number }>`
672
+
673
+ Get count of pending reports (admin only).
674
+
675
+ ```typescript
676
+ const { count } = await sdk.reports.getPendingCount();
677
+ console.log(`${count} pending reports`);
678
+ ```
679
+
680
+ #### `update(id: string, data: UpdateReportData): Promise<Report>`
681
+
682
+ Update a report status/notes (admin only).
683
+
684
+ ```typescript
685
+ const report = await sdk.reports.update('report-id', {
686
+ status: 'RESOLVED',
687
+ adminNotes: 'Reviewed and user has been warned',
688
+ });
689
+ ```
690
+
691
+ **Parameters:**
692
+
693
+ - `id: string` - The report ID
694
+ - `data.status?: ReportStatus` - New status (PENDING, READ, RESOLVED, DISMISSED)
695
+ - `data.adminNotes?: string` - Admin notes
696
+
697
+ #### `delete(id: string): Promise<void>`
698
+
699
+ Delete a report (admin only).
700
+
701
+ ```typescript
702
+ await sdk.reports.delete('report-id');
703
+ ```
704
+
705
+ ### Report Types
706
+
707
+ ```typescript
708
+ type ReportStatus = 'PENDING' | 'READ' | 'RESOLVED' | 'DISMISSED';
709
+
710
+ interface Report {
711
+ id: string;
712
+ reporterId: string;
713
+ reportedUserId: string;
714
+ reason: string;
715
+ status: ReportStatus;
716
+ adminNotes?: string;
717
+ createdAt: string;
718
+ updatedAt: string;
719
+ reporter?: ReportUser;
720
+ reportedUser?: ReportUser;
721
+ }
722
+
723
+ interface ReportUser {
724
+ id: string;
725
+ username?: string;
726
+ avatar?: string;
727
+ }
728
+ ```
729
+
730
+ ---
731
+
732
+ ## Support Tickets (`sdk.support`)
733
+
734
+ Built-in support ticket system for communicating with the DIM team. Users and agents can create tickets, add follow-up messages, and track resolution. Admin methods are also available for managing all tickets.
735
+
736
+ ### `create(data): Promise<SupportTicket>`
737
+
738
+ Create a new support ticket. Rate limited to 5 per hour.
739
+
740
+ ```typescript
741
+ const ticket = await sdk.support.create({
742
+ message: 'I cannot withdraw my USDC balance',
743
+ category: 'PAYMENT', // optional, default: OTHER
744
+ subject: 'Withdrawal Issue', // optional, auto-generated from category
745
+ email: 'me@example.com', // optional contact email
746
+ });
747
+ console.log(`Ticket #${ticket.id}: ${ticket.subject} [${ticket.status}]`);
748
+ ```
749
+
750
+ ### `getMyTickets(options?): Promise<PaginatedSupportTickets>`
751
+
752
+ List your own tickets with optional filters.
753
+
754
+ ```typescript
755
+ const { tickets, total } = await sdk.support.getMyTickets({
756
+ status: 'OPEN', // optional
757
+ category: 'BUG', // optional
758
+ page: 1,
759
+ limit: 10,
760
+ });
761
+ ```
762
+
763
+ ### `getMyTicketById(ticketId): Promise<SupportTicket>`
764
+
765
+ Get a specific ticket with all messages.
766
+
767
+ ```typescript
768
+ const ticket = await sdk.support.getMyTicketById('ticket-uuid');
769
+ ticket.messages?.forEach((msg) => {
770
+ console.log(`[${msg.senderRole}] ${msg.content}`);
771
+ });
772
+ ```
773
+
774
+ ### `addMessage(ticketId, message): Promise<SupportMessage>`
775
+
776
+ Add a follow-up message to your ticket.
777
+
778
+ ```typescript
779
+ await sdk.support.addMessage('ticket-uuid', 'Here is more detail...');
780
+ ```
781
+
782
+ ### `closeTicket(ticketId): Promise<SupportTicket>`
783
+
784
+ Close a ticket when your issue is resolved.
785
+
786
+ ```typescript
787
+ await sdk.support.closeTicket('ticket-uuid');
788
+ ```
789
+
790
+ ### Admin Methods
791
+
792
+ ```typescript
793
+ // List all tickets (admin only)
794
+ const all = await sdk.support.list({ status: 'OPEN', page: 1, limit: 10 });
795
+
796
+ // Get open ticket count (admin only)
797
+ const { count } = await sdk.support.getOpenCount();
798
+
799
+ // Get any ticket by ID (admin only)
800
+ const ticket = await sdk.support.getById('ticket-uuid');
801
+
802
+ // Update status/priority (admin only)
803
+ await sdk.support.update('ticket-uuid', {
804
+ status: 'IN_PROGRESS',
805
+ priority: 'HIGH',
806
+ });
807
+
808
+ // Reply to a ticket (admin only)
809
+ await sdk.support.reply('ticket-uuid', 'We are looking into this.');
810
+
811
+ // Delete a ticket (admin only)
812
+ await sdk.support.delete('ticket-uuid');
813
+ ```
814
+
815
+ ### Support Types
816
+
817
+ ```typescript
818
+ type SupportTicketStatus =
819
+ | 'OPEN'
820
+ | 'IN_PROGRESS'
821
+ | 'WAITING_REPLY'
822
+ | 'RESOLVED'
823
+ | 'CLOSED';
824
+ type SupportTicketCategory =
825
+ | 'BUG'
826
+ | 'FEATURE_REQUEST'
827
+ | 'QUESTION'
828
+ | 'ACCOUNT'
829
+ | 'PAYMENT'
830
+ | 'GAME'
831
+ | 'TECHNICAL'
832
+ | 'OTHER';
833
+ type SupportTicketPriority = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';
834
+ type SupportMessageSenderRole = 'USER' | 'ADMIN';
835
+
836
+ interface SupportTicket {
837
+ id: string;
838
+ userId: string;
839
+ subject: string;
840
+ category: SupportTicketCategory;
841
+ status: SupportTicketStatus;
842
+ priority: SupportTicketPriority;
843
+ createdAt: string;
844
+ updatedAt: string;
845
+ user?: SupportTicketUser;
846
+ messages?: SupportMessage[];
847
+ messageCount?: number;
848
+ }
849
+
850
+ interface SupportMessage {
851
+ id: string;
852
+ ticketId: string;
853
+ senderId: string;
854
+ senderRole: SupportMessageSenderRole;
855
+ content: string;
856
+ createdAt: string;
857
+ sender?: SupportTicketUser;
858
+ }
859
+ ```
860
+
861
+ ---
862
+
863
+ ## Challenges (`sdk.challenges`)
864
+
865
+ Standalone API for creating and accepting game challenges. Any user can challenge any other user (by id or username). Global chat `/challenge` is a shorthand that uses the same backend; use `sdk.challenges` when you want to create or accept challenges outside of chat (e.g. from a profile page or notifications).
866
+
867
+ **Game fees:** Accepted challenges create a lobby with the chosen amount. Game fees apply: 1% per player, minimum 1 cent per player.
868
+
869
+ ### `create(dto: CreateChallengeRequest): Promise<CreateChallengeResponse>`
870
+
871
+ Create a challenge.
872
+
873
+ ```typescript
874
+ const result = await sdk.challenges.create({
875
+ gameType: 'rock-paper-scissors',
876
+ amount: 500_000, // USDC minor units; min $0.10 (100_000), no max
877
+ targetUserId: 'user-id', // or use targetUsername
878
+ targetUsername: 'alice',
879
+ });
880
+ // result: { challengeId, challengerId, targetUserId, gameType, amountMinor, challengerUsername? }
881
+ ```
882
+
883
+ - **amount:** USDC minor units (6 decimals). Minimum $0.10 (100_000), no maximum.
884
+ - **targetUserId** or **targetUsername:** The user to challenge (username can include or omit `@`).
885
+
886
+ ### `accept(challengeId: string): Promise<AcceptChallengeResponse>`
887
+
888
+ Accept a challenge. Only the challenged user can accept. Creates a lobby with the challenge amount and adds both users.
889
+
890
+ ```typescript
891
+ const result = await sdk.challenges.accept('challenge-id');
892
+ // result: { lobbyId, gameId?, status }
893
+ ```
894
+
895
+ ---
896
+
897
+ ## Wallet Module
898
+
899
+ The wallet module provides functionality for managing Solana wallets, checking balances, and signing transactions. **Transfers and tips** incur a 1 cent fee per transfer/tip; the fee is shown before you confirm. The SDK uses a **pluggable signer interface** that allows different signing implementations (e.g., Phantom wallet, Keypair for bots) to be used.
900
+
901
+ ### WalletSigner Interface
902
+
903
+ The SDK exposes a `WalletSigner` interface for transaction and message signing:
904
+
905
+ ```typescript
906
+ interface WalletSigner {
907
+ signMessage: (message: string) => Promise<Uint8Array | string>;
908
+ signTransaction: (transaction: Transaction) => Promise<Transaction>;
909
+ }
910
+ ```
911
+
912
+ This allows the SDK to remain agnostic about how signing is implemented - you can use Phantom wallet for browser apps, or a Keypair for bot applications.
913
+
914
+ If you're building agents, you can use `@dimcool/wallet` and pass `wallet.getSigner()` directly:
915
+
916
+ ```typescript
917
+ import { Wallet } from '@dimcool/wallet';
918
+
919
+ const wallet = new Wallet({
920
+ enabledNetworks: ['solana'],
921
+ fromPrivateKey: process.env.DIM_WALLET_PRIVATE_KEY!,
922
+ });
923
+
924
+ sdk.wallet.setSigner(wallet.getSigner());
925
+ ```
926
+
927
+ ### `setSigner(signer: WalletSigner): void`
928
+
929
+ Configure the wallet signer. This must be called before any signing operations.
930
+
931
+ ```typescript
932
+ // Example: Using Phantom wallet (in apps/web)
933
+ import { useSolana } from '@phantom/react-sdk';
934
+
935
+ const { solana } = useSolana();
936
+
937
+ sdk.wallet.setSigner({
938
+ signMessage: async (message: string) => {
939
+ return solana.signMessage(new TextEncoder().encode(message));
940
+ },
941
+ signTransaction: async (transaction: Transaction) => {
942
+ return solana.signTransaction(transaction);
943
+ },
944
+ });
945
+ ```
946
+
947
+ ```typescript
948
+ // Example: Using a Keypair (for bots/Node.js)
949
+ import { Keypair } from '@solana/web3.js';
950
+ import * as nacl from 'tweetnacl';
951
+
952
+ const keypair = Keypair.fromSecretKey(/* ... */);
953
+
954
+ sdk.wallet.setSigner({
955
+ signMessage: async (message: string) => {
956
+ const messageBytes = new TextEncoder().encode(message);
957
+ return nacl.sign.detached(messageBytes, keypair.secretKey);
958
+ },
959
+ signTransaction: async (transaction: Transaction) => {
960
+ transaction.partialSign(keypair);
961
+ return transaction;
962
+ },
963
+ });
964
+ ```
965
+
966
+ ### `hasSigner(): boolean`
967
+
968
+ Check if a signer has been configured.
969
+
970
+ ```typescript
971
+ if (!sdk.wallet.hasSigner()) {
972
+ // Configure signer before signing operations
973
+ sdk.wallet.setSigner(mySigner);
974
+ }
975
+ ```
976
+
977
+ ### `loadWallet(): Promise<WalletResponse>`
978
+
979
+ Load the user's wallet data from the API.
980
+
981
+ ```typescript
982
+ const wallet = await sdk.wallet.loadWallet();
983
+ // wallet: { publicKey: string }
984
+ ```
985
+
986
+ **Response format:**
987
+
988
+ ```typescript
989
+ interface WalletResponse {
990
+ publicKey: string; // Base58 encoded Solana public key
991
+ }
992
+ ```
993
+
994
+ **Example:**
995
+
996
+ ```typescript
997
+ const wallet = await sdk.wallet.loadWallet();
998
+ console.log(`Public key: ${wallet.publicKey}`);
999
+ ```
1000
+
1001
+ ### `getBalances(): Promise<BalanceResponse>`
1002
+
1003
+ Get the SOL and USDC balances for the user's wallet.
1004
+
1005
+ ```typescript
1006
+ const balances = await sdk.wallet.getBalances();
1007
+ // balances: { sol: number, usdc: number, publicKey: string }
1008
+ ```
1009
+
1010
+ **Response format:**
1011
+
1012
+ ```typescript
1013
+ interface BalanceResponse {
1014
+ sol: number; // SOL balance
1015
+ usdc: number; // USDC balance (6 decimals)
1016
+ publicKey: string; // Wallet public key
1017
+ }
1018
+ ```
1019
+
1020
+ **Example:**
1021
+
1022
+ ```typescript
1023
+ const balances = await sdk.wallet.getBalances();
1024
+ console.log(`SOL: ${balances.sol}`);
1025
+ console.log(`USDC: ${balances.usdc}`);
1026
+ ```
1027
+
1028
+ ### `signMessage(message: string): Promise<Uint8Array | string>`
1029
+
1030
+ Sign a message using the configured signer.
1031
+
1032
+ ```typescript
1033
+ const signature = await sdk.wallet.signMessage('Hello, world!');
1034
+ ```
1035
+
1036
+ **Parameters:**
1037
+
1038
+ - `message: string` - The message to sign
1039
+
1040
+ **Returns:**
1041
+
1042
+ - `Promise<Uint8Array | string>` - The signature
1043
+
1044
+ **Note:** A signer must be configured first using `setSigner()`.
1045
+
1046
+ ### `signTransaction(transaction: Transaction): Promise<Transaction>`
1047
+
1048
+ Sign a Solana transaction using the configured signer.
1049
+
1050
+ ```typescript
1051
+ import { Transaction } from '@solana/web3.js';
1052
+
1053
+ const unsignedTransaction = Transaction.from(/* ... */);
1054
+ const signedTransaction = await sdk.wallet.signTransaction(unsignedTransaction);
1055
+ ```
1056
+
1057
+ **Parameters:**
1058
+
1059
+ - `transaction: Transaction` - The unsigned Solana transaction to sign
1060
+
1061
+ **Returns:**
1062
+
1063
+ - `Promise<Transaction>` - The signed transaction
1064
+
1065
+ **Example:**
1066
+
1067
+ ```typescript
1068
+ // Transaction should be prepared by the backend first
1069
+ const { unsignedTransaction } = await sdk.http.post(
1070
+ '/transactions/prepare-deposit',
1071
+ {
1072
+ lobbyId: 'lobby-id',
1073
+ amount: 100,
1074
+ },
1075
+ );
1076
+
1077
+ const tx = Transaction.from(Buffer.from(unsignedTransaction, 'base64'));
1078
+ const signedTx = await sdk.wallet.signTransaction(tx);
1079
+ ```
1080
+
1081
+ **Note:** A signer must be configured first using `setSigner()`.
1082
+
1083
+ ### `send(recipient, amount, token?)`
1084
+
1085
+ One-call transfer flow. This method prepares, signs (with configured signer), and submits the transaction internally.
1086
+
1087
+ ```typescript
1088
+ const result = await sdk.wallet.send('alice.sol', 1_000_000, 'USDC');
1089
+ // result: { signature, status, recipientAddress, fee, totalAmount, token }
1090
+ ```
1091
+
1092
+ - `recipient`: DIM username, `.sol` domain, or Solana address
1093
+ - `amount`: minor units (USDC) or lamports (SOL)
1094
+ - `token`: `'USDC' | 'SOL'` (default `'USDC'`)
1095
+
1096
+ **Note:** `setSigner(...)` must be configured before calling `send(...)`.
1097
+
1098
+ ### Transfer recipient formats
1099
+
1100
+ Wallet send (`sdk.wallet.send(...)`) accepts:
1101
+
1102
+ - DIM username (e.g. `alice`)
1103
+ - Solana address (base58)
1104
+ - `.sol` domain (e.g. `alice.sol`)
1105
+
1106
+ For `.sol` recipients, the API resolves the SNS domain to its current owner address during the prepare step and returns the resolved `recipientAddress` in the response.
1107
+
1108
+ ### `getAdminActivity(params?): Promise<AdminWalletActivityResponse>` (Admin only)
1109
+
1110
+ Get paginated wallet activity across all users. Admin only. Optional filters: by kind (DEPOSIT, REFUND, PAYOUT, TRANSFER), by userId; sort by date (asc/desc).
1111
+
1112
+ ```typescript
1113
+ const result = await sdk.wallet.getAdminActivity({
1114
+ limit: 20,
1115
+ cursor: 'optional-cursor-from-previous-page',
1116
+ kinds: ['DEPOSIT', 'TRANSFER'],
1117
+ userId: 'user-id',
1118
+ sortOrder: 'desc',
1119
+ });
1120
+ // result: { items: AdminWalletActivityItem[], nextCursor?: string }
1121
+ ```
1122
+
1123
+ **Parameters:**
1124
+
1125
+ - `params.limit?` - Page size (default 20, max 100)
1126
+ - `params.cursor?` - Opaque cursor for next page
1127
+ - `params.kinds?` - Filter by activity kinds (array)
1128
+ - `params.userId?` - Filter to transactions involving this user (from or to)
1129
+ - `params.sortOrder?` - `'asc'` or `'desc'` (default newest first)
1130
+
1131
+ **Response:** `AdminWalletActivityResponse` with `items` (each item includes `fromUserId`, `toUserId`, `fromUsername`, `toUsername`, plus standard activity fields) and optional `nextCursor`.
1132
+
1133
+ ---
1134
+
1135
+ ## Tips Module (`sdk.tips`)
1136
+
1137
+ ### `send(recipientUsername, amount)`
1138
+
1139
+ One-call tip flow: prepares tip transaction, signs + submits transfer via configured wallet signer, then broadcasts the tip to global chat.
1140
+
1141
+ ```typescript
1142
+ const tip = await sdk.tips.send('bob', 1_000_000); // $1.00
1143
+ // tip: { signature, status, recipientAddress, recipientUserId, recipientUsername, amount, fee, totalAmount, message }
1144
+ ```
1145
+
1146
+ ### Low-level methods (optional)
1147
+
1148
+ - `prepare({ recipientUsername, amount })`
1149
+ - `broadcast({ recipientUserId, amount })`
1150
+
1151
+ Use these only when you explicitly need manual control of signing/submission.
1152
+
1153
+ ## Escrow Module (`sdk.escrow`)
1154
+
1155
+ The escrow module provides functionality for managing deposits, checking deposit status, and handling the escrow flow for game lobbies. This module works in conjunction with the wallet module to enable secure, blockchain-based deposits.
1156
+
1157
+ ### `startDeposits(lobbyId: string): Promise<void>`
1158
+
1159
+ Start the deposit flow for a lobby. This transitions the lobby from `waiting` to `preparing` state and initializes deposit tracking for all players.
1160
+
1161
+ ```typescript
1162
+ await sdk.escrow.startDeposits('lobby-id');
1163
+ ```
1164
+
1165
+ **Requirements:**
1166
+
1167
+ - User must be a member of the lobby
1168
+ - Lobby must be in `waiting` state
1169
+ - Lobby must have a `betAmount` configured
1170
+ - Lobby must be full (all players joined)
1171
+
1172
+ **Behavior:**
1173
+
1174
+ - Transitions lobby status to `preparing`
1175
+ - Initializes deposit status tracking for all players
1176
+ - Emits WebSocket event `lobby:updated`
1177
+
1178
+ **Example:**
1179
+
1180
+ ```typescript
1181
+ // Start deposits when lobby is ready
1182
+ await sdk.escrow.startDeposits(lobbyId);
1183
+ console.log('Deposit flow started');
1184
+ ```
1185
+
1186
+ ### `prepareDepositTransaction(lobbyId: string): Promise<PrepareDepositResponse>`
1187
+
1188
+ Prepare an unsigned deposit transaction for the current user. The transaction must be signed by the user's wallet before submission.
1189
+
1190
+ ```typescript
1191
+ const response = await sdk.escrow.prepareDepositTransaction('lobby-id');
1192
+ // response: { transaction: string, message: string }
1193
+ ```
1194
+
1195
+ **Response format:**
1196
+
1197
+ ```typescript
1198
+ interface PrepareDepositResponse {
1199
+ transaction: string; // Base64-encoded unsigned Solana transaction
1200
+ message: string; // Human-readable message about the transaction
1201
+ }
1202
+ ```
1203
+
1204
+ **Example:**
1205
+
1206
+ ```typescript
1207
+ // Prepare the deposit transaction
1208
+ const { transaction, message } =
1209
+ await sdk.escrow.prepareDepositTransaction(lobbyId);
1210
+
1211
+ // Decode and sign the transaction
1212
+ const binaryString = atob(transaction);
1213
+ const bytes = new Uint8Array(binaryString.length);
1214
+ for (let i = 0; i < binaryString.length; i++) {
1215
+ bytes[i] = binaryString.charCodeAt(i);
1216
+ }
1217
+ const unsignedTx = Transaction.from(bytes);
1218
+ const signedTx = await sdk.wallet.signTransaction(unsignedTx);
1219
+ ```
1220
+
1221
+ **Note:** The transaction amount is automatically set to match the lobby's `betAmount`. The transaction is partially signed by the fee payer (server) and requires the user's signature.
1222
+
1223
+ ### `submitDepositAndJoinQueue(lobbyId: string, signedTransaction: string, lobbies: Lobbies): Promise<Lobby>`
1224
+
1225
+ Submit a signed deposit transaction and automatically join the queue when the deposit is confirmed. This is a convenience method that combines deposit submission, confirmation waiting, and queue joining.
1226
+
1227
+ ```typescript
1228
+ const finalLobby = await sdk.escrow.submitDepositAndJoinQueue(
1229
+ lobbyId,
1230
+ signedTxBase64,
1231
+ sdk.lobbies,
1232
+ );
1233
+ ```
1234
+
1235
+ **Parameters:**
1236
+
1237
+ - `lobbyId: string` - The lobby ID
1238
+ - `signedTransaction: string` - The signed transaction (base64 encoded)
1239
+ - `lobbies: Lobbies` - The lobbies service instance (from `sdk.lobbies`)
1240
+
1241
+ **Returns:**
1242
+
1243
+ - `Promise<Lobby>` - The lobby with updated status (will be 'queued' or 'active' if full)
1244
+
1245
+ **Behavior:**
1246
+
1247
+ 1. Submits the signed deposit transaction to the Solana network
1248
+ 2. Polls deposit status until all deposits are confirmed (max 30 seconds)
1249
+ 3. Automatically joins the matchmaking queue when deposits are confirmed
1250
+ 4. Returns the updated lobby
1251
+
1252
+ **Example:**
1253
+
1254
+ ```typescript
1255
+ // After signing the transaction from playAgain()
1256
+ const signedTxBase64 = signedTx.serialize().toString('base64');
1257
+ const finalLobby = await sdk.escrow.submitDepositAndJoinQueue(
1258
+ lobby.id,
1259
+ signedTxBase64,
1260
+ sdk.lobbies,
1261
+ );
1262
+
1263
+ // Lobby is now in queue or active if full
1264
+ console.log(`Lobby status: ${finalLobby.status}`);
1265
+ ```
1266
+
1267
+ **Error Handling:**
1268
+
1269
+ - Throws an error if deposit confirmation times out (30 seconds)
1270
+ - Throws an error if deposit submission fails
1271
+ - Throws an error if queue joining fails
1272
+
1273
+ **Note:** This method is typically used after `playAgain()` to complete the "Play Again" flow in one call.
1274
+
1275
+ ### `submitDeposit(lobbyId: string, signedTransaction: string): Promise<SubmitDepositResponse>`
1276
+
1277
+ Submit a signed deposit transaction to the Solana network. The transaction will be confirmed before the method returns.
1278
+
1279
+ ```typescript
1280
+ const signedTxBase64 = signedTx.serialize().toString('base64');
1281
+ const response = await sdk.escrow.submitDeposit(lobbyId, signedTxBase64);
1282
+ // response: { signature: string, status: string }
1283
+ ```
1284
+
1285
+ **Response format:**
1286
+
1287
+ ```typescript
1288
+ interface SubmitDepositResponse {
1289
+ signature: string; // Solana transaction signature
1290
+ status: string; // Transaction status ('confirmed' on success)
1291
+ }
1292
+ ```
1293
+
1294
+ **Example:**
1295
+
1296
+ ```typescript
1297
+ // Sign the transaction
1298
+ const signedTx = await sdk.wallet.signTransaction(unsignedTx);
1299
+
1300
+ // Serialize and submit
1301
+ const signedTxBase64 = signedTx.serialize().toString('base64');
1302
+ const { signature, status } = await sdk.escrow.submitDeposit(
1303
+ lobbyId,
1304
+ signedTxBase64,
1305
+ );
1306
+
1307
+ console.log(`Deposit confirmed: ${signature}`);
1308
+ ```
1309
+
1310
+ **Behavior:**
1311
+
1312
+ - Submits transaction to Solana network
1313
+ - Waits for transaction confirmation
1314
+ - Updates deposit status to `confirmed` on success
1315
+ - Automatically transitions lobby to `queued` when all deposits are confirmed
1316
+ - Marks deposit as `failed` on error
1317
+
1318
+ ### `getDepositStatus(lobbyId: string): Promise<DepositStatusResponse>`
1319
+
1320
+ Get the deposit status for all players in a lobby.
1321
+
1322
+ ```typescript
1323
+ const status = await sdk.escrow.getDepositStatus('lobby-id');
1324
+ // status: DepositStatusResponse
1325
+ ```
1326
+
1327
+ **Response format:**
1328
+
1329
+ ```typescript
1330
+ interface DepositStatusResponse {
1331
+ deposits: Array<{
1332
+ userId: string;
1333
+ status: 'pending' | 'signed' | 'confirmed' | 'failed';
1334
+ transactionHash?: string;
1335
+ signedAt?: string;
1336
+ confirmedAt?: string;
1337
+ errorMessage?: string;
1338
+ }>;
1339
+ allConfirmed: boolean; // True if all deposits are confirmed
1340
+ canProceedToQueue: boolean; // True if lobby can proceed to queue
1341
+ }
1342
+ ```
1343
+
1344
+ **Example:**
1345
+
1346
+ ```typescript
1347
+ const status = await sdk.escrow.getDepositStatus(lobbyId);
1348
+
1349
+ // Check if all deposits are confirmed
1350
+ if (status.allConfirmed) {
1351
+ console.log('All deposits confirmed!');
1352
+ }
1353
+
1354
+ // Check individual player status
1355
+ status.deposits.forEach((deposit) => {
1356
+ console.log(`Player ${deposit.userId}: ${deposit.status}`);
1357
+ if (deposit.transactionHash) {
1358
+ console.log(` Transaction: ${deposit.transactionHash}`);
1359
+ }
1360
+ });
1361
+ ```
1362
+
1363
+ **Deposit Status Values:**
1364
+
1365
+ - `pending` - Deposit not yet prepared or signed
1366
+ - `signed` - Transaction signed but not yet confirmed on-chain
1367
+ - `confirmed` - Transaction confirmed on Solana network
1368
+ - `failed` - Transaction failed or timed out
1369
+
1370
+ ### Complete Deposit Flow Example
1371
+
1372
+ ```typescript
1373
+ import { SDK, BrowserLocalStorage } from '@dimcool/sdk';
1374
+ import { Transaction } from '@solana/web3.js';
1375
+
1376
+ const sdk = new SDK({
1377
+ appId: 'dim',
1378
+ baseUrl: 'https://api.example.com',
1379
+ storage: new BrowserLocalStorage(),
1380
+ });
1381
+
1382
+ // 1. Authenticate
1383
+ await sdk.auth.login('user@example.com', 'password');
1384
+
1385
+ // 2. Load wallet
1386
+ await sdk.wallet.loadWallet();
1387
+
1388
+ // 3. Start deposit flow (lobby creator only)
1389
+ await sdk.escrow.startDeposits(lobbyId);
1390
+
1391
+ // 4. Prepare deposit transaction
1392
+ const { transaction } = await sdk.escrow.prepareDepositTransaction(lobbyId);
1393
+
1394
+ // 5. Decode and sign transaction
1395
+ const binaryString = atob(transaction);
1396
+ const bytes = new Uint8Array(binaryString.length);
1397
+ for (let i = 0; i < binaryString.length; i++) {
1398
+ bytes[i] = binaryString.charCodeAt(i);
1399
+ }
1400
+ const unsignedTx = Transaction.from(bytes);
1401
+ const signedTx = await sdk.wallet.signTransaction(unsignedTx);
1402
+
1403
+ // 6. Submit signed transaction
1404
+ const signedTxBase64 = signedTx.serialize().toString('base64');
1405
+ const { signature } = await sdk.escrow.submitDeposit(lobbyId, signedTxBase64);
1406
+
1407
+ console.log(`Deposit confirmed: ${signature}`);
1408
+
1409
+ // 7. Check status (optional, for polling)
1410
+ const status = await sdk.escrow.getDepositStatus(lobbyId);
1411
+ if (status.allConfirmed) {
1412
+ console.log('All players have confirmed deposits!');
1413
+ }
1414
+ ```
1415
+
1416
+ **Notes:**
1417
+
1418
+ - Deposits are automatically refunded if the deposit flow times out (5 minutes)
1419
+ - If any deposit fails, confirmed deposits are automatically refunded
1420
+ - The lobby automatically transitions to `queued` when all deposits are confirmed
1421
+ - Transaction hashes are stored in `GameHistory` for permanent records
1422
+
1423
+ ---
1424
+
1425
+ ## Feature Flags (`sdk.featureFlags`)
1426
+
1427
+ ### `getFeatureFlags(): Promise<FeatureFlag[]>`
1428
+
1429
+ Get all feature flags. Flags are cached locally after the first call.
1430
+
1431
+ ```typescript
1432
+ const flags = await sdk.featureFlags.getFeatureFlags();
1433
+ // flags: [{ id: string, name: string, enabled: boolean, createdAt: string, updatedAt: string }]
1434
+ ```
1435
+
1436
+ ### `isEnabledFlag(name: string): boolean`
1437
+
1438
+ Check if a feature flag is enabled. Returns `false` if the flag doesn't exist or hasn't been loaded yet.
1439
+
1440
+ ```typescript
1441
+ if (sdk.featureFlags.isEnabledFlag('enable-rock-paper-scissors')) {
1442
+ // Feature is enabled
1443
+ }
1444
+ ```
1445
+
1446
+ ### `isLoaded(): boolean`
1447
+
1448
+ Check if feature flags have been loaded.
1449
+
1450
+ ```typescript
1451
+ if (sdk.featureFlags.isLoaded()) {
1452
+ // Flags are available
1453
+ }
1454
+ ```
1455
+
1456
+ ### `updateFeatureFlag(name: string, enabled: boolean): Promise<FeatureFlag>`
1457
+
1458
+ Update a feature flag. Admin only.
1459
+
1460
+ ```typescript
1461
+ const updatedFlag = await sdk.featureFlags.updateFeatureFlag('my-flag', true);
1462
+ ```
1463
+
1464
+ ### `createFeatureFlag(name: string, enabled: boolean): Promise<FeatureFlag>`
1465
+
1466
+ Create a new feature flag. Admin only.
1467
+
1468
+ ```typescript
1469
+ const newFlag = await sdk.featureFlags.createFeatureFlag('new-feature', false);
1470
+ ```
1471
+
1472
+ ---
1473
+
1474
+ ## Lobbies (`sdk.lobbies`)
1475
+
1476
+ ### `createLobby(gameType: string): Promise<Lobby>`
1477
+
1478
+ Create a new game lobby.
1479
+
1480
+ ```typescript
1481
+ const lobby = await sdk.lobbies.createLobby('rock-paper-scissors');
1482
+ // lobby: { id, gameType, status, creatorId, maxPlayers, players: [...], ... }
1483
+ ```
1484
+
1485
+ ### `getLobby(lobbyId: string): Promise<Lobby>`
1486
+
1487
+ Get lobby details. Only accessible to lobby members. The returned `Lobby` includes `players[].usdcBalance` (optional, USDC in minor units, cached) for displaying balances and gating Start Game when a player has insufficient funds.
1488
+
1489
+ ```typescript
1490
+ const lobby = await sdk.lobbies.getLobby('lobby-id');
1491
+ ```
1492
+
1493
+ ### `inviteFriend(lobbyId: string, friendId: string): Promise<{ message: string }>`
1494
+
1495
+ Invite a friend to a lobby. Creator only.
1496
+
1497
+ ```typescript
1498
+ await sdk.lobbies.inviteFriend('lobby-id', 'friend-user-id');
1499
+ ```
1500
+
1501
+ ### `joinLobby(lobbyId: string): Promise<Lobby>`
1502
+
1503
+ Join a lobby without requiring an invitation. Any authenticated user can join a lobby if:
1504
+
1505
+ - The lobby is in `waiting` status
1506
+ - The lobby is not full
1507
+ - The user is not already in the lobby
1508
+
1509
+ ```typescript
1510
+ const lobby = await sdk.lobbies.joinLobby('lobby-id');
1511
+ ```
1512
+
1513
+ **Behavior:**
1514
+
1515
+ - Adds the user to the lobby
1516
+ - Emits `lobby:player:joined` WebSocket event
1517
+ - Returns updated lobby with the new player
1518
+
1519
+ **Note:** This is different from `acceptInvite()` which is used when accepting a friend invitation. `joinLobby()` allows direct joining via shared lobby URLs.
1520
+
1521
+ ### `acceptInvite(lobbyId: string): Promise<Lobby>`
1522
+
1523
+ Accept a lobby invitation.
1524
+
1525
+ ```typescript
1526
+ const lobby = await sdk.lobbies.acceptInvite('lobby-id');
1527
+ ```
1528
+
1529
+ ### `getLobby(lobbyId: string): Promise<Lobby>`
1530
+
1531
+ Get lobby details. Any authenticated user can view lobby details, even if not a member. This allows users to see lobby info before deciding to join.
1532
+
1533
+ The returned `Lobby` includes `players[].usdcBalance` (optional, number, USDC in minor units, cached) so the app can display each player's balance and gate "Start Game" when a player has insufficient funds.
1534
+
1535
+ ```typescript
1536
+ const lobby = await sdk.lobbies.getLobby('lobby-id');
1537
+ ```
1538
+
1539
+ ### `removePlayer(lobbyId: string, userId: string): Promise<{ message: string }>`
1540
+
1541
+ Remove a player from a lobby. Can be used by the player to leave or by the creator to kick.
1542
+
1543
+ ```typescript
1544
+ await sdk.lobbies.removePlayer('lobby-id', 'user-id');
1545
+ ```
1546
+
1547
+ ### `kickPlayer(lobbyId: string, userId: string): Promise<{ message: string }>`
1548
+
1549
+ Kick a player from a lobby. Creator only. The creator can kick any player except themselves.
1550
+
1551
+ ```typescript
1552
+ await sdk.lobbies.kickPlayer('lobby-id', 'user-id');
1553
+ ```
1554
+
1555
+ **Behavior:**
1556
+
1557
+ - Removes the player from the lobby
1558
+ - Emits `lobby:player:left` WebSocket event
1559
+ - If the lobby was in queue, the queue is automatically cancelled
1560
+ - Kicked users can rejoin the lobby using `joinLobby()`
1561
+
1562
+ ### `leaveLobby(lobbyId: string): Promise<{ message: string }>`
1563
+
1564
+ Leave a lobby. Alias for removing yourself. If the lobby is in queue, it will be automatically cancelled.
1565
+
1566
+ ```typescript
1567
+ await sdk.lobbies.leaveLobby('lobby-id');
1568
+ ```
1569
+
1570
+ ### Queue Management
1571
+
1572
+ #### `joinQueue(lobbyId: string): Promise<Lobby>`
1573
+
1574
+ Join the matchmaking queue for a lobby, or start the game immediately if the lobby is full. The lobby must be in `waiting` state.
1575
+
1576
+ ```typescript
1577
+ const lobby = await sdk.lobbies.joinQueue('lobby-id');
1578
+ // If full: lobby.status will be 'active'
1579
+ // If not full: lobby.status will be 'queued'
1580
+ ```
1581
+
1582
+ **Requirements:**
1583
+
1584
+ - Lobby must be in `waiting` state
1585
+ - User must be a member of the lobby
1586
+ **WebSocket connection is required** - User must be connected via the realtime transport
1587
+ - If not connected, the call will fail with a `BadRequestException`
1588
+ - Preferred: `await sdk.ensureWebSocketConnected()` or `sdk.wsTransport.connect()`
1589
+ - Join the lobby room: `socket.emit('join-lobby', lobbyId)`
1590
+
1591
+ **Note on Lobby Creation vs Queue:**
1592
+
1593
+ - Lobbies can be created via HTTP (no WebSocket required) - useful for programmatic/admin creation
1594
+ - However, to join the matchmaking queue, a WebSocket connection to the root namespace is required
1595
+ - This ensures only connected users can enter matchmaking, preventing orphaned queue entries
1596
+
1597
+ **Behavior:**
1598
+
1599
+ - **If lobby is full** (`players.length === maxPlayers`):
1600
+ - Game starts immediately
1601
+ - Lobby status changes to `active`
1602
+ - `gameId` is created and assigned
1603
+ - WebSocket event `lobby:matched` is emitted (with single lobby in `matchedLobbies`)
1604
+ - Returns lobby with `status: 'active'`
1605
+
1606
+ - **If lobby is not full**:
1607
+ - Lobby status changes to `queued`
1608
+ - Lobby is added to Redis queue for matching
1609
+ - WebSocket event `lobby:queue:joined` is emitted
1610
+ - System attempts to find a match immediately
1611
+ - Returns lobby with `status: 'queued'`
1612
+
1613
+ **Note**: The same endpoint works for both scenarios. The system automatically determines whether to start the game immediately or join the queue based on lobby capacity. The lobby creator clicks "Start Game" and the system handles the rest.
1614
+
1615
+ #### `cancelQueue(lobbyId: string): Promise<Lobby>`
1616
+
1617
+ Cancel the queue for a lobby. The lobby must be in `queued` state.
1618
+
1619
+ ```typescript
1620
+ const lobby = await sdk.lobbies.cancelQueue('lobby-id');
1621
+ // lobby.status will be 'waiting'
1622
+ ```
1623
+
1624
+ **Requirements:**
1625
+
1626
+ - Lobby must be in `queued` state
1627
+ - User must be a member of the lobby
1628
+
1629
+ **Behavior:**
1630
+
1631
+ - Lobby is removed from Redis queue
1632
+ - Lobby status changes back to `waiting`
1633
+ - WebSocket event `lobby:queue:cancelled` is emitted
1634
+
1635
+ **Note:** The queue is automatically cancelled if any player leaves the lobby while it's in queue.
1636
+
1637
+ #### `playAgain(gameType: string, betAmount: number, escrow: Escrow): Promise<{ lobby: Lobby; unsignedTransaction: string }>`
1638
+
1639
+ Create a new lobby, start deposits, and prepare a deposit transaction for playing again with the same bet amount. This is a convenience method that combines lobby creation, deposit initialization, and transaction preparation.
1640
+
1641
+ ```typescript
1642
+ const { lobby, unsignedTransaction } = await sdk.lobbies.playAgain(
1643
+ 'rock-paper-scissors',
1644
+ 25,
1645
+ sdk.escrow,
1646
+ );
1647
+ ```
1648
+
1649
+ **Parameters:**
1650
+
1651
+ - `gameType: string` - The game type to play again (e.g., 'rock-paper-scissors')
1652
+ - `betAmount: number` - The bet amount (same as previous game)
1653
+ - `escrow: Escrow` - The escrow service instance (from `sdk.escrow`)
1654
+
1655
+ **Returns:**
1656
+
1657
+ - `Promise<{ lobby: Lobby; unsignedTransaction: string }>` - The created lobby and unsigned transaction (base64 encoded) that needs to be signed
1658
+
1659
+ **Behavior:**
1660
+
1661
+ 1. Creates a new lobby with the specified game type and bet amount
1662
+ 2. Starts the deposit flow (transitions lobby to 'preparing' state)
1663
+ 3. Prepares the deposit transaction
1664
+ 4. Returns the lobby and unsigned transaction
1665
+
1666
+ **Example:**
1667
+
1668
+ ```typescript
1669
+ // Play again with same bet amount
1670
+ const { lobby, unsignedTransaction } = await sdk.lobbies.playAgain(
1671
+ 'rock-paper-scissors',
1672
+ 25,
1673
+ sdk.escrow,
1674
+ );
1675
+
1676
+ // Decode and sign the transaction
1677
+ const binaryString = atob(unsignedTransaction);
1678
+ const bytes = new Uint8Array(binaryString.length);
1679
+ for (let i = 0; i < binaryString.length; i++) {
1680
+ bytes[i] = binaryString.charCodeAt(i);
1681
+ }
1682
+ const unsignedTx = Transaction.from(bytes);
1683
+ const signedTx = await sdk.wallet.signTransaction(unsignedTx);
1684
+
1685
+ // Submit deposit and join queue
1686
+ const signedTxBase64 = signedTx.serialize().toString('base64');
1687
+ const finalLobby = await sdk.escrow.submitDepositAndJoinQueue(
1688
+ lobby.id,
1689
+ signedTxBase64,
1690
+ sdk.lobbies,
1691
+ );
1692
+ ```
1693
+
1694
+ **Note:** After signing the transaction, use `submitDepositAndJoinQueue()` to complete the flow and automatically join the matchmaking queue.
1695
+
1696
+ ### Admin Methods
1697
+
1698
+ #### `getActiveLobbies(): Promise<Lobby[]>` (Admin Only)
1699
+
1700
+ Get all active lobbies. Admin only.
1701
+
1702
+ ```typescript
1703
+ const activeLobbies = await sdk.lobbies.getActiveLobbies();
1704
+ ```
1705
+
1706
+ #### `getQueueStats(): Promise<QueueStats>` (Admin Only)
1707
+
1708
+ Get queue statistics for the admin dashboard. Admin only.
1709
+
1710
+ ```typescript
1711
+ const stats = await sdk.lobbies.getQueueStats();
1712
+ // stats: {
1713
+ // totalPlayersInQueue: number,
1714
+ // queueSizes: Record<string, number>, // Key: 'queue:gameType:requiredPlayers', Value: count
1715
+ // totalQueuedLobbies: number
1716
+ // }
1717
+ ```
1718
+
1719
+ ---
1720
+
1721
+ ## Chat (`sdk.chat`)
1722
+
1723
+ The chat system is a unified, generic chat service that supports multiple contexts: lobbies, games, and direct messages (DMs). All chat operations use the generic chat endpoints.
1724
+
1725
+ ### Context Types
1726
+
1727
+ - `lobby` - Chat within a lobby
1728
+ - `game` - Chat within a game
1729
+ - `dm` - Direct message between two users (contextId is the other user's ID)
1730
+ - `global` - App-wide chat; single room for all authenticated users (contextId is `global`)
1731
+
1732
+ ### Global chat
1733
+
1734
+ Global chat uses the same API with context `{ type: 'global', id: 'global' }`.
1735
+
1736
+ - **Subscribe:** Call `chatStore.joinContext({ type: 'global', id: 'global' })` (or use a hook that does this on mount). Room name: `chat:global:global`.
1737
+ - **Unsubscribe:** Call the function returned from `joinContext`, or unmount the component that subscribes.
1738
+ - **Load snapshot:** `getChatHistory` / `getChatHistoryPaginated({ type: 'global', id: 'global' }, limit)` returns the last N messages; `getChatHistoryPaginated` returns `{ messages, nextCursor: null }` (no cursor pagination for global).
1739
+ - **Send:** `sendMessage({ type: 'global', id: 'global' }, message, replyTo?, gifUrl?, clientMessageId?)`. If the message starts with `/`, the server may interpret it as a command (e.g. `/help`, `/challenge`, `/tip`). See API docs for command syntax.
1740
+ - **Reactions:** Same as other contexts — `addReaction` / `removeReaction` with context `{ type: 'global', id: 'global' }` and `messageId`.
1741
+ - **Global-only methods:** `acceptGlobalChallenge(challengeId)` to accept a challenge from chat; `broadcastGlobalTip(recipientUserId, amount)` to broadcast a tip message after the user has signed and submitted the transfer.
1742
+
1743
+ ### `sendMessage(context: ChatContext, message: string, replyTo?: ChatMessageReply, gifUrl?: string): Promise<ChatMessage>`
1744
+
1745
+ Send a chat message to any context.
1746
+
1747
+ ```typescript
1748
+ // Send to lobby
1749
+ const message = await sdk.chat.sendMessage(
1750
+ { type: 'lobby', id: 'lobby-id' },
1751
+ 'Hello, everyone!',
1752
+ );
1753
+
1754
+ // Send with reply
1755
+ const reply = await sdk.chat.sendMessage(
1756
+ { type: 'lobby', id: 'lobby-id' },
1757
+ 'This is a reply',
1758
+ {
1759
+ id: originalMessage.id,
1760
+ userId: originalMessage.userId,
1761
+ username: originalMessage.username,
1762
+ message: originalMessage.message,
1763
+ },
1764
+ );
1765
+
1766
+ // Send with GIF
1767
+ const gifMessage = await sdk.chat.sendMessage(
1768
+ { type: 'lobby', id: 'lobby-id' },
1769
+ '',
1770
+ undefined,
1771
+ 'https://media.giphy.com/test.gif',
1772
+ );
1773
+ ```
1774
+
1775
+ **Parameters:**
1776
+
1777
+ - `context` - Chat context (`{ type: 'lobby' | 'game' | 'dm' | 'global', id: string }`)
1778
+ - `message` - Message text (1-500 characters, or empty if gifUrl provided)
1779
+ - `replyTo` - Optional. Reference to the message being replied to
1780
+ - `gifUrl` - Optional. Giphy GIF URL
1781
+
1782
+ **Returns:** `ChatMessage` object
1783
+
1784
+ ### `getChatHistory(context: ChatContext, limit?: number): Promise<ChatMessage[]>`
1785
+
1786
+ Get chat history for any context.
1787
+
1788
+ ```typescript
1789
+ // Get lobby chat history
1790
+ const history = await sdk.chat.getChatHistory(
1791
+ { type: 'lobby', id: 'lobby-id' },
1792
+ 50,
1793
+ );
1794
+
1795
+ // Get game chat history
1796
+ const gameHistory = await sdk.chat.getChatHistory(
1797
+ { type: 'game', id: 'game-id' },
1798
+ 100,
1799
+ );
1800
+ ```
1801
+
1802
+ **Parameters:**
1803
+
1804
+ - `context` - Chat context
1805
+ - `limit` - Optional. Maximum number of messages (default: 50)
1806
+
1807
+ **Returns:** Array of `ChatMessage` objects in chronological order (oldest first)
1808
+
1809
+ ### `addReaction(context: ChatContext, messageId: string, emoji: string): Promise<ChatMessage>`
1810
+
1811
+ Add a reaction to a message.
1812
+
1813
+ ```typescript
1814
+ const updated = await sdk.chat.addReaction(
1815
+ { type: 'lobby', id: 'lobby-id' },
1816
+ 'message-id',
1817
+ '👍',
1818
+ );
1819
+ ```
1820
+
1821
+ **Returns:** Updated `ChatMessage` object with the new reaction
1822
+
1823
+ ### `removeReaction(context: ChatContext, messageId: string, emoji: string): Promise<ChatMessage>`
1824
+
1825
+ Remove a reaction from a message.
1826
+
1827
+ ```typescript
1828
+ const updated = await sdk.chat.removeReaction(
1829
+ { type: 'lobby', id: 'lobby-id' },
1830
+ 'message-id',
1831
+ '👍',
1832
+ );
1833
+ ```
1834
+
1835
+ **Returns:** Updated `ChatMessage` object with the reaction removed
1836
+
1837
+ ### `markAsRead(context: ChatContext, messageId: string): Promise<ChatMessage>`
1838
+
1839
+ Mark a message as read.
1840
+
1841
+ ```typescript
1842
+ const updated = await sdk.chat.markAsRead(
1843
+ { type: 'lobby', id: 'lobby-id' },
1844
+ 'message-id',
1845
+ );
1846
+ ```
1847
+
1848
+ **Returns:** Updated `ChatMessage` object with read receipt
1849
+
1850
+ ---
1851
+
1852
+ ## Notifications (`sdk.notifications`)
1853
+
1854
+ The notifications module provides methods for listing and managing the current user's app notifications (friend requests, game invites, achievements, etc.), plus admin-only broadcast/send.
1855
+
1856
+ ### `list(params?: { page?, limit? }): Promise<PaginatedNotificationsResponse>`
1857
+
1858
+ List notifications for the current user (paginated).
1859
+
1860
+ ```typescript
1861
+ const res = await sdk.notifications.list({ page: 1, limit: 20 });
1862
+ // res.notifications, res.total, res.page, res.limit, res.unreadCount
1863
+ ```
1864
+
1865
+ ### `markAsRead(id: string): Promise<AppNotification>`
1866
+
1867
+ Mark a single notification as read.
1868
+
1869
+ ### `markAllAsRead(): Promise<void>`
1870
+
1871
+ Mark all notifications as read.
1872
+
1873
+ ### `dismiss(id: string): Promise<void>`
1874
+
1875
+ Dismiss (delete) a notification.
1876
+
1877
+ ### `broadcastNotification(message: string): Promise<void>`
1878
+
1879
+ Broadcast a notification to all connected users (admin only).
1880
+
1881
+ ```typescript
1882
+ await sdk.notifications.broadcastNotification(
1883
+ 'Server maintenance in 10 minutes',
1884
+ );
1885
+ ```
1886
+
1887
+ **Parameters:**
1888
+
1889
+ - `message` - Notification message (1-500 characters)
1890
+
1891
+ **Note:** This endpoint requires admin authentication. The notification will be sent to all users currently connected to the application via the root WebSocket namespace.
1892
+
1893
+ ### `sendToUser(userId: string, message: string): Promise<void>`
1894
+
1895
+ Send a notification to a specific user (admin only).
1896
+
1897
+ ```typescript
1898
+ await sdk.notifications.sendToUser('user-id', 'You have a new game invite!');
1899
+ ```
1900
+
1901
+ **Parameters:**
1902
+
1903
+ - `userId` - Target user ID
1904
+ - `message` - Notification message (1-500 characters)
1905
+
1906
+ **Note:** This endpoint requires admin authentication. The notification will be sent to the specified user if they are currently connected.
1907
+
1908
+ ### WebSocket Notifications Channel
1909
+
1910
+ Subscribe via the event bus to receive real-time notification events:
1911
+
1912
+ ```typescript
1913
+ sdk.wsTransport.connect();
1914
+
1915
+ // Listen for notification events
1916
+ sdk.events.subscribe('notification', (event) => {
1917
+ console.log('Notification received:', event);
1918
+ // event: { title: string, body?: string, icon?: string, tag?: string, data?: any }
1919
+ });
1920
+ ```
1921
+
1922
+ **Notification Event Structure:** When the server sends an app notification (e.g. friend request, game invite, achievement), the payload may include the full list-item shape so the client can render the panel without an extra fetch:
1923
+
1924
+ ```typescript
1925
+ // Generic toast shape
1926
+ interface NotificationEvent {
1927
+ title: string;
1928
+ body?: string;
1929
+ icon?: string;
1930
+ tag?: string;
1931
+ data?: any;
1932
+ timestamp?: string;
1933
+ }
1934
+ // App notification payload (when present) also includes:
1935
+ // id: string, type: AppNotificationType, message: string, read: boolean,
1936
+ // createdAt: string, actionPayload?: object, fromUserId?, fromUsername?, avatar?
1937
+ ```
1938
+
1939
+ **Note:** The notifications WebSocket channel requires authentication. Users must be authenticated to receive notifications. The client should check if notifications are enabled and browser permissions are granted before showing browser notifications. All WebSocket features (lobbies, games, chat, notifications) share a single connection to the root namespace for better performance.
1940
+
1941
+ ---
1942
+
1943
+ ## Achievements (`sdk.achievements`)
1944
+
1945
+ Methods for listing achievement definitions and a user's unlocked achievements (e.g. for profile badges).
1946
+
1947
+ ### `list(): Promise<Achievement[]>`
1948
+
1949
+ List all achievement definitions (code, name, description, category, icon).
1950
+
1951
+ ```typescript
1952
+ const all = await sdk.achievements.list();
1953
+ ```
1954
+
1955
+ ### `getMyAchievements(): Promise<UserAchievement[]>`
1956
+
1957
+ Get the current user's unlocked achievements (code, name, description, icon, unlockedAt). Requires authentication.
1958
+
1959
+ ### `getForUser(userId: string): Promise<UserAchievement[]>`
1960
+
1961
+ Get unlocked achievements for a given user (e.g. for public profile).
1962
+
1963
+ ```typescript
1964
+ const badges = await sdk.achievements.getForUser('user-id');
1965
+ ```
1966
+
1967
+ ---
1968
+
1969
+ ## Activity Feed (`sdk.activity`)
1970
+
1971
+ ### `getFeed(limit?: number): Promise<ActivityFeedResponse>`
1972
+
1973
+ Fetch the global activity feed (new user signups, wins, lobbies created, and
1974
+ games starting). Results are cached server-side for 15 minutes.
1975
+
1976
+ ```typescript
1977
+ const feed = await sdk.activity.getFeed(15);
1978
+ feed.items.forEach((item) => {
1979
+ console.log(item.type, item.user.username, item.game);
1980
+ });
1981
+ ```
1982
+
1983
+ ---
1984
+
1985
+ ## Leaderboards (`sdk.leaderboards`)
1986
+
1987
+ Fetch leaderboard data for global, per-game, or friends leaderboards with
1988
+ date-range filtering. Friends leaderboard requires authentication.
1989
+
1990
+ ### `getGlobalLeaderboard(query?: { startDate?: string; endDate?: string; limit?: number })`
1991
+
1992
+ ```typescript
1993
+ const global = await sdk.leaderboards.getGlobalLeaderboard({
1994
+ startDate: '2024-01-01',
1995
+ endDate: '2024-01-31',
1996
+ limit: 20,
1997
+ });
1998
+ ```
1999
+
2000
+ ### `getGameLeaderboard(gameType: string, query?: { startDate?: string; endDate?: string; limit?: number })`
2001
+
2002
+ ```typescript
2003
+ const game = await sdk.leaderboards.getGameLeaderboard('rock-paper-scissors', {
2004
+ startDate: '2024-01-01',
2005
+ endDate: '2024-01-31',
2006
+ });
2007
+ ```
2008
+
2009
+ ### `getFriendsLeaderboard(query?: { startDate?: string; endDate?: string; limit?: number })`
2010
+
2011
+ ```typescript
2012
+ const friends = await sdk.leaderboards.getFriendsLeaderboard({
2013
+ startDate: '2024-01-01',
2014
+ endDate: '2024-01-31',
2015
+ });
2016
+ ```
2017
+
2018
+ **Leaderboard entry fields:** `gameEarnings`, `referralEarnings`, `earnings`, `wins`, `losses`, `winRate`
2019
+
2020
+ ---
2021
+
2022
+ ### ChatMessage Interface
2023
+
2024
+ ```typescript
2025
+ interface ChatMessage {
2026
+ id: string;
2027
+ lobbyId: string; // Empty string for game/DM contexts
2028
+ userId?: string; // Optional for system messages
2029
+ username?: string; // Optional for system messages
2030
+ message: string;
2031
+ type: 'user' | 'system';
2032
+ systemType?: 'player_joined' | 'player_left' | 'bet_changed';
2033
+ replyTo?: {
2034
+ id: string;
2035
+ userId?: string;
2036
+ username?: string;
2037
+ message: string;
2038
+ };
2039
+ reactions?: Array<{
2040
+ emoji: string;
2041
+ userId: string;
2042
+ username?: string;
2043
+ createdAt: string;
2044
+ }>;
2045
+ readBy?: Array<{
2046
+ userId: string;
2047
+ readAt: string;
2048
+ }>;
2049
+ metadata?: Record<string, any>; // e.g., { gifUrl: string } or { betAmount: number }
2050
+ createdAt: string;
2051
+ }
2052
+ ```
2053
+
2054
+ ### WebSocket Chat Events
2055
+
2056
+ All WebSocket features use a single connection to the root namespace. The namespace parameter is kept for backward compatibility but is ignored - all events come from the same connection.
2057
+
2058
+ Connect to the root namespace to receive real-time chat events (all features share a single connection):
2059
+
2060
+ **Preferred:** use `sdk.chatStore` for updates. The following direct WebSocket examples are legacy.
2061
+
2062
+ ```typescript
2063
+ sdk.wsTransport.connect();
2064
+
2065
+ // Listen for new messages
2066
+ sdk.events.subscribe('chat:message', (message: ChatMessage) => {
2067
+ console.log('New message:', message);
2068
+ });
2069
+
2070
+ // Listen for reaction updates
2071
+ sdk.events.subscribe('chat:reaction', (message: ChatMessage) => {
2072
+ console.log('Reaction updated:', message);
2073
+ });
2074
+
2075
+ // Listen for read receipts
2076
+ sdk.events.subscribe('chat:read', (data) => {
2077
+ console.log('Message read:', data.messageId, 'by', data.userId);
2078
+ });
2079
+
2080
+ // Listen for typing indicators
2081
+ sdk.events.subscribe('chat:typing', (data) => {
2082
+ if (data.typing) {
2083
+ console.log(`${data.username} is typing...`);
2084
+ } else {
2085
+ console.log(`${data.username} stopped typing`);
2086
+ }
2087
+ });
2088
+ ```
2089
+
2090
+ **Events:**
2091
+
2092
+ - `chat:message` - New message received (user or system)
2093
+ - `chat:reaction` - Reaction added or removed
2094
+ - `chat:read` - Message marked as read
2095
+ - `chat:typing` - User started/stopped typing
2096
+
2097
+ ### Examples
2098
+
2099
+ ```typescript
2100
+ // Send a message to a lobby
2101
+ const message = await sdk.chat.sendMessage(
2102
+ { type: 'lobby', id: 'lobby-id' },
2103
+ 'Hello!',
2104
+ );
2105
+
2106
+ // Reply to a message
2107
+ const reply = await sdk.chat.sendMessage(
2108
+ { type: 'lobby', id: 'lobby-id' },
2109
+ 'This is a reply',
2110
+ {
2111
+ id: message.id,
2112
+ userId: message.userId,
2113
+ username: message.username,
2114
+ message: message.message,
2115
+ },
2116
+ );
2117
+
2118
+ // Add a reaction
2119
+ await sdk.chat.addReaction({ type: 'lobby', id: 'lobby-id' }, message.id, '👍');
2120
+
2121
+ // Get chat history
2122
+ const history = await sdk.chat.getChatHistory(
2123
+ { type: 'lobby', id: 'lobby-id' },
2124
+ 50,
2125
+ );
2126
+
2127
+ // Mark as read
2128
+ await sdk.chat.markAsRead({ type: 'lobby', id: 'lobby-id' }, message.id);
2129
+ ```
2130
+
2131
+ ---
2132
+
2133
+ ## Referrals (`sdk.referrals`)
2134
+
2135
+ DIM has a 3-level referral system that pays passive income in USDC. Commission rates: Level 1 = 30%, Level 2 = 3%, Level 3 = 2% of game fees.
2136
+
2137
+ ### `getSummary(): Promise<ReferralSummary>`
2138
+
2139
+ Get your referral summary including code, link, totals per level, and earnings.
2140
+
2141
+ ```typescript
2142
+ const summary = await sdk.referrals.getSummary();
2143
+ // {
2144
+ // code: 'my-username',
2145
+ // link: 'https://dim.cool/?ref=my-username',
2146
+ // totals: { level1: 15, level2: 3, level3: 1 },
2147
+ // earnings: { pending: 500000, claimed: 2000000 }
2148
+ // }
2149
+ ```
2150
+
2151
+ ### `getTree(params?): Promise<ReferralTreeResponse>`
2152
+
2153
+ Get your referral tree at a specific level.
2154
+
2155
+ ```typescript
2156
+ const tree = await sdk.referrals.getTree({ level: 1, limit: 50 });
2157
+ // { level: 1, items: [{ id, username, createdAt }], nextCursor?: string }
2158
+
2159
+ // Paginate
2160
+ const nextPage = await sdk.referrals.getTree({
2161
+ level: 1,
2162
+ limit: 50,
2163
+ cursor: tree.nextCursor,
2164
+ });
2165
+ ```
2166
+
2167
+ **Parameters:**
2168
+
2169
+ | Name | Type | Description |
2170
+ | -------- | ------------- | -------------------------------- |
2171
+ | `level` | `1 \| 2 \| 3` | Referral level to view |
2172
+ | `limit` | `number` | Max results (1-200, default: 50) |
2173
+ | `cursor` | `string` | Pagination cursor |
2174
+
2175
+ ### `getRewards(params?): Promise<ReferralRewardsResponse>`
2176
+
2177
+ Get referral reward history with optional status filter.
2178
+
2179
+ ```typescript
2180
+ // Get pending rewards
2181
+ const rewards = await sdk.referrals.getRewards({ status: 'PENDING' });
2182
+ // { items: [{ id, referredUserId, referredUsername, level, amount, status, createdAt }], nextCursor? }
2183
+
2184
+ // Get all rewards
2185
+ const all = await sdk.referrals.getRewards({ limit: 100 });
2186
+ ```
2187
+
2188
+ **Parameters:**
2189
+
2190
+ | Name | Type | Description |
2191
+ | -------- | --------------------------------------- | ------------------- |
2192
+ | `status` | `'PENDING' \| 'CLAIMED' \| 'CANCELLED'` | Filter by status |
2193
+ | `limit` | `number` | Max results (1-200) |
2194
+ | `cursor` | `string` | Pagination cursor |
2195
+
2196
+ ### `claimRewards(): Promise<ClaimReferralRewardsResponse>`
2197
+
2198
+ Claim all pending referral rewards. USDC is transferred from escrow to your wallet.
2199
+
2200
+ ```typescript
2201
+ const result = await sdk.referrals.claimRewards();
2202
+ // { claimedCount: 5, claimedAmount: 1500000, walletTransactionSignature: '5xY...' }
2203
+ console.log(`Claimed $${(result.claimedAmount / 1_000_000).toFixed(2)} USDC`);
2204
+ ```
2205
+
2206
+ ---
2207
+
2208
+ ## Games (`sdk.games`)
2209
+
2210
+ ### `getAvailableGames(): Promise<GameType[]>`
2211
+
2212
+ Get all available game types. Only returns game types that are enabled via feature flags.
2213
+
2214
+ ```typescript
2215
+ const games = await sdk.games.getAvailableGames();
2216
+ // games: [{ id: string, name: string, maxPlayers: number, minPlayers: number, description?: string }]
2217
+ ```
2218
+
2219
+ **Example:**
2220
+
2221
+ ```typescript
2222
+ const availableGames = await sdk.games.getAvailableGames();
2223
+ console.log('Available games:', availableGames);
2224
+ // Output: [{ id: 'rock-paper-scissors', name: 'Rock Paper Scissors', maxPlayers: 2, minPlayers: 2, ... }]
2225
+
2226
+ // Use game type when creating a lobby
2227
+ if (availableGames.length > 0) {
2228
+ const lobby = await sdk.lobbies.createLobby(availableGames[0].id);
2229
+ }
2230
+ ```
2231
+
2232
+ **Note:** Game types are feature-flagged. A game type will only appear in this list if its corresponding feature flag (e.g., `enable-rock-paper-scissors`) is enabled.
2233
+
2234
+ ### `getGame(gameId: string): Promise<Game>`
2235
+
2236
+ Get game details by game ID. Returns game information including players, status, and associated lobbies.
2237
+
2238
+ ```typescript
2239
+ const game = await sdk.games.getGame('game-123');
2240
+ // game: { gameId: string, gameType: string, status: 'active' | 'completed' | 'abandoned', createdAt: string, players: GamePlayer[], lobbyIds: string[] }
2241
+ ```
2242
+
2243
+ **Example:**
2244
+
2245
+ ```typescript
2246
+ try {
2247
+ const game = await sdk.games.getGame('game-123');
2248
+ console.log('Game:', game.gameType, game.status);
2249
+ console.log(
2250
+ 'Players:',
2251
+ game.players.map((p) => p.username),
2252
+ );
2253
+ console.log('Lobbies:', game.lobbyIds);
2254
+ } catch (error) {
2255
+ if (error instanceof Error) {
2256
+ console.error('Game not found:', error.message);
2257
+ }
2258
+ }
2259
+ ```
2260
+
2261
+ **GamePlayer interface:**
2262
+
2263
+ ```typescript
2264
+ interface GamePlayer {
2265
+ userId: string;
2266
+ username: string;
2267
+ name: string;
2268
+ }
2269
+ ```
2270
+
2271
+ ### `getLiveGames(): Promise<Game[]>`
2272
+
2273
+ Get list of currently active (live) games for spectating. Public endpoint; no auth required.
2274
+
2275
+ ```typescript
2276
+ const liveGames = await sdk.games.getLiveGames();
2277
+ // liveGames: Game[] - same shape as getGame()
2278
+ ```
2279
+
2280
+ ## Spectate (`sdk.spectate`)
2281
+
2282
+ Spectate discovery — finding who to watch.
2283
+
2284
+ ### `sdk.spectate.getLivePlayers(options?): Promise<LivePlayersPage>`
2285
+
2286
+ Get list of players currently in an active game, **paginated and sorted by newest**. Samples a limited number of games per page (default 10) so the API stays fast with many active games. Returns **one entry per unique player** in the sample (no duplicates). Use for "Live now" and "Who to watch" UI; link to `/spectate/:username` for each player.
2287
+
2288
+ **Parameters (all optional):**
2289
+
2290
+ | Option | Type | Description |
2291
+ | -------- | ------ | -------------------------------------------------------------------------- |
2292
+ | `limit` | number | Number of games to sample per page (1–20, default 10). |
2293
+ | `cursor` | string | Opaque cursor from the previous response’s `nextCursor` for the next page. |
2294
+
2295
+ **Returns:** `Promise<LivePlayersPage>` with `items: LivePlayer[]` and `nextCursor: string | null` (non-null when there are more games to page).
2296
+
2297
+ ```typescript
2298
+ const page = await sdk.spectate.getLivePlayers({ limit: 10 });
2299
+ // page: { items: LivePlayer[], nextCursor: string | null }
2300
+ // Next page: sdk.spectate.getLivePlayers({ limit: 10, cursor: page.nextCursor })
2301
+ ```
2302
+
2303
+ **LivePlayer and LivePlayersPage:**
2304
+
2305
+ ```typescript
2306
+ interface LivePlayer {
2307
+ userId: string;
2308
+ username: string;
2309
+ avatar?: string;
2310
+ gameId: string;
2311
+ gameType: string;
2312
+ spectatorCount: number; // unique users currently watching this player
2313
+ }
2314
+
2315
+ interface LivePlayersPage {
2316
+ items: LivePlayer[];
2317
+ nextCursor: string | null;
2318
+ }
2319
+ ```
2320
+
2321
+ > **Note:** `spectatorCount` is the number of **unique users currently watching this player** (connected to the spectate channel for that user). It is not a per-game counter.
2322
+
2323
+ ### Spectate channel (WebSocket)
2324
+
2325
+ When on the spectate page (e.g. `/spectate/:username`), the client should:
2326
+
2327
+ 1. **Join the game room** (for game events, chat, and pot): `gameStore.joinGame(gameId)`.
2328
+ 2. **Join the spectate channel** (to be counted as a viewer of that user): `gameStore.joinSpectateChannel(spectatedUserId)`.
2329
+
2330
+ Both use the same existing socket connection (no new connection). The spectate channel is reference-counted: calling `joinSpectateChannel` returns a leave function. Call it (or disconnect) to stop being counted. If you switch from spectating UserA to UserB, the server automatically leaves UserA's channel first.
2331
+
2332
+ ```typescript
2333
+ // On spectate page mount
2334
+ const leaveGame = sdk.gameStore.joinGame(gameId);
2335
+ const leaveSpectate = sdk.gameStore.joinSpectateChannel(spectatedUserId);
2336
+
2337
+ // On spectate page unmount
2338
+ leaveSpectate();
2339
+ leaveGame();
2340
+ ```
2341
+
2342
+ #### Real-time spectator count
2343
+
2344
+ The server emits `spectator:count:updated` events whenever a viewer joins or leaves. The SDK `gameStore` tracks these counts automatically:
2345
+
2346
+ ```typescript
2347
+ // Read from the store (snapshot)
2348
+ const count = sdk.gameStore.getSpectatorCount(userId);
2349
+
2350
+ // Subscribe reactively (in React — see useSpectatorCount hook in react-sdk)
2351
+ const { count } = useSpectatorCount({
2352
+ userId: spectatedUserId,
2353
+ joinChannel: true,
2354
+ });
2355
+ ```
2356
+
2357
+ #### REST spectator count
2358
+
2359
+ ```typescript
2360
+ // Get the game-level spectator count (union of all players' viewers)
2361
+ const { count } = await sdk.games.getGameSpectatorCount(gameId);
2362
+ ```
2363
+
2364
+ - **Chat**: Spectators can send and receive messages in game chat. Join the chat room: `chat:join` with `{ type: 'game', id: gameId }`. Messages from non-players have `metadata.role = 'spectator'`.
2365
+ - **Pot (donations)**: Spectators can donate via `POST /games/:gameId/donate` (same as players). The amount is added to the spectator pot.
2366
+
2367
+ ### Spectator donations
2368
+
2369
+ #### `prepareGameDonation(gameId: string, amountMinor: number): Promise<{ transaction, escrowAddress, amountMinor }>`
2370
+
2371
+ Prepare a USDC transfer to the game pot (escrow). Returns an unsigned transaction for the client to sign. Minimum 10 cents (100,000 minor units). Call `donateToGame` with the signed transaction after signing.
2372
+
2373
+ #### `donateToGame(gameId: string, amountMinor: number, signedTransaction: string): Promise<{ signature, status }>`
2374
+
2375
+ Submit a signed game donation. The amount is added to the spectator pot; the winner receives player pot + spectator donations minus 1% fee. A system message is posted in game chat.
2376
+
2377
+ ### Game Action Methods
2378
+
2379
+ #### `submitAction(gameId: string, action: ValidAction): Promise<void>`
2380
+
2381
+ Submit a typed action for the current game. Only game players can submit actions.
2382
+
2383
+ ```typescript
2384
+ await sdk.games.submitAction('game-id', {
2385
+ gameType: 'rock-paper-scissors',
2386
+ action: 'play',
2387
+ payload: { action: 'rock' },
2388
+ });
2389
+ ```
2390
+
2391
+ ```typescript
2392
+ // Chess move
2393
+ await sdk.games.submitAction('game-id', {
2394
+ gameType: 'chess',
2395
+ action: 'move',
2396
+ payload: { from: 'e2', to: 'e4' },
2397
+ });
2398
+ ```
2399
+
2400
+ **Requirements:**
2401
+
2402
+ - User must be a player in the game
2403
+ - Game must be active
2404
+ - RPS: round must be in selection phase and not yet submitted
2405
+ - Other games: validation depends on the game engine
2406
+
2407
+ **Behavior:**
2408
+
2409
+ - Action is stored in game state
2410
+ - WebSocket event `game:rps:action:received` is emitted to all players
2411
+ - If both players act before timer expires, timer cuts short and reveal phase starts immediately
2412
+ - If timer expires before action, random action is chosen automatically
2413
+
2414
+ **Example:**
2415
+
2416
+ ```typescript
2417
+ // Submit action during selection phase
2418
+ try {
2419
+ await sdk.games.submitAction('game-id', {
2420
+ gameType: 'rock-paper-scissors',
2421
+ action: 'play',
2422
+ payload: { action: 'paper' },
2423
+ });
2424
+ console.log('Action submitted successfully');
2425
+ } catch (error) {
2426
+ if (error instanceof Error) {
2427
+ console.error('Failed to submit action:', error.message);
2428
+ }
2429
+ }
2430
+ ```
2431
+
2432
+ #### `getGameState(gameId: string): Promise<GameStateResponse>`
2433
+
2434
+ Get current game state with timer information, round details, scores, and actions. Works for both **players and spectators** (same endpoint and response shape); only players can submit actions.
2435
+
2436
+ ```typescript
2437
+ const state = await sdk.games.getGameState('game-id');
2438
+ // state: GameStateResponse
2439
+ ```
2440
+
2441
+ **Returns:** Current game state including:
2442
+
2443
+ - Round information (current round, max rounds, phase)
2444
+ - Timer information (time remaining in milliseconds)
2445
+ - Actions (your action visible, opponent's action hidden until reveal)
2446
+ - Scores (wins per player)
2447
+ - Winner ID (if game completed)
2448
+
2449
+ **GameStateResponse interface:**
2450
+
2451
+ ```typescript
2452
+ type GameStateResponse =
2453
+ | RpsGameState
2454
+ | ChessGameState
2455
+ | NimGameState
2456
+ | DotsAndBoxesGameState
2457
+ | TicTacToeGameState;
2458
+ ```
2459
+
2460
+ **Example:**
2461
+
2462
+ ```typescript
2463
+ const state = await sdk.games.getGameState('game-id');
2464
+
2465
+ console.log(`Round ${state.currentRound} of ${state.maxRounds}`);
2466
+ console.log(`Phase: ${state.roundState.phase}`);
2467
+ console.log(
2468
+ `Time remaining: ${Math.ceil(state.roundState.timeRemaining / 1000)}s`,
2469
+ );
2470
+ console.log(`Scores:`, state.scores);
2471
+
2472
+ // Check if you can submit an action
2473
+ if (state.roundState.phase === 'selection') {
2474
+ const myAction = state.roundState.actions[userId];
2475
+ if (!myAction.submitted) {
2476
+ // Submit action
2477
+ await sdk.games.submitAction('game-id', {
2478
+ gameType: 'rock-paper-scissors',
2479
+ action: 'play',
2480
+ payload: { action: 'rock' },
2481
+ });
2482
+ }
2483
+ }
2484
+ ```
2485
+
2486
+ ### WebSocket Game Events
2487
+
2488
+ **Preferred:** use `sdk.gameStore` / `sdk.gameActionsStore` for updates. Use the event bus for raw event streams.
2489
+
2490
+ #### `game:move` (Chess)
2491
+
2492
+ Emitted when a chess move is applied.
2493
+
2494
+ ```typescript
2495
+ sdk.events.subscribe('game:move', (data) => {
2496
+ if (data.gameType === 'chess') {
2497
+ console.log(`${data.playerId} moved ${data.move.san}`);
2498
+ }
2499
+ });
2500
+ ```
2501
+
2502
+ #### `game:rps:round:started`
2503
+
2504
+ Emitted when a new round begins.
2505
+
2506
+ ```typescript
2507
+ sdk.events.subscribe('game:rps:round:started', (data) => {
2508
+ console.log(`Round ${data.roundNumber} started`);
2509
+ console.log(`Selection ends at: ${data.selectionEndsAt}`);
2510
+ console.log(`Time remaining: ${data.timeRemaining}ms`);
2511
+ });
2512
+ ```
2513
+
2514
+ #### `game:rps:action:received`
2515
+
2516
+ Emitted when a player has submitted (choice is not revealed until `game:rps:round:reveal`). Payload: `gameId`, `roundNumber`, `playerId`, `submittedAt` (no `action`).
2517
+
2518
+ ```typescript
2519
+ sdk.events.subscribe('game:rps:action:received', (data) => {
2520
+ console.log(`Player ${data.playerId} submitted`);
2521
+ });
2522
+ ```
2523
+
2524
+ #### `game:rps:timer:cutoff`
2525
+
2526
+ Emitted when timer is cut short because both players acted early.
2527
+
2528
+ ```typescript
2529
+ sdk.events.subscribe('game:rps:timer:cutoff', (data) => {
2530
+ console.log('Timer cut short, reveal phase starting');
2531
+ });
2532
+ ```
2533
+
2534
+ #### `game:rps:round:reveal`
2535
+
2536
+ Emitted when reveal phase starts (both actions are known).
2537
+
2538
+ ```typescript
2539
+ sdk.events.subscribe('game:rps:round:reveal', (data) => {
2540
+ console.log('Actions revealed:', data.actions);
2541
+ // data.actions: { [userId]: 'rock' | 'paper' | 'scissors' }
2542
+ });
2543
+ ```
2544
+
2545
+ #### `game:rps:round:completed`
2546
+
2547
+ Emitted when a round finishes.
2548
+
2549
+ ```typescript
2550
+ sdk.events.subscribe('game:rps:round:completed', (data) => {
2551
+ console.log(`Round ${data.roundNumber} winner: ${data.winnerId || 'Tie'}`);
2552
+ console.log('Scores:', data.scores);
2553
+ });
2554
+ ```
2555
+
2556
+ #### `game:rps:timeout`
2557
+
2558
+ Emitted when timer expires and random action is chosen.
2559
+
2560
+ ```typescript
2561
+ sdk.events.subscribe('game:rps:timeout', (data) => {
2562
+ console.log(
2563
+ `Player ${data.playerId} timed out, random action: ${data.action}`,
2564
+ );
2565
+ });
2566
+ ```
2567
+
2568
+ #### `game:completed`
2569
+
2570
+ Emitted when the match finishes.
2571
+
2572
+ ```typescript
2573
+ sdk.events.subscribe('game:completed', (data) => {
2574
+ if (data.isDraw) {
2575
+ console.log('Game ended in a draw');
2576
+ } else {
2577
+ console.log(`Game winner: ${data.winnerId}`);
2578
+ }
2579
+ if (data.gameType === 'rock-paper-scissors') {
2580
+ console.log(`Final scores:`, data.finalScores);
2581
+ console.log(`Rounds:`, data.rounds);
2582
+ }
2583
+ console.log(`Won amount: $${data.wonAmount}`);
2584
+ });
2585
+ ```
2586
+
2587
+ ---
2588
+
2589
+ ## Prediction Markets (`sdk.markets`)
2590
+
2591
+ The markets module provides functionality for prediction market trading on games. Uses a Constant Product Market Maker (CPMM) for instant execution — no order matching needed. Users can buy and sell outcome shares, view positions with P&L, and redeem winnings after resolution. Admin methods provide aggregate analytics.
2592
+
2593
+ ### `getMarket(gameId: string): Promise<MarketState>`
2594
+
2595
+ Get the prediction market state for a game. Returns prices, volume, collateral, and resolution status.
2596
+
2597
+ ```typescript
2598
+ const market = await sdk.markets.getMarket('game-id');
2599
+ console.log(market.prices); // { playerAId: 0.6, playerBId: 0.4 }
2600
+ console.log(market.totalVolume); // "5000000"
2601
+ console.log(market.status); // "OPEN" or "RESOLVED"
2602
+ ```
2603
+
2604
+ ### `getMyPositions(gameId: string): Promise<MarketPosition[]>`
2605
+
2606
+ Get the authenticated user's positions for a game. Returns shares held, average cost, current value, and unrealized P&L.
2607
+
2608
+ ```typescript
2609
+ const positions = await sdk.markets.getMyPositions('game-id');
2610
+ positions.forEach((pos) => {
2611
+ console.log(`Outcome: ${pos.outcomeId}`);
2612
+ console.log(`Shares: ${pos.shares}`);
2613
+ console.log(`Current value: ${pos.currentValue}`);
2614
+ console.log(`Unrealized P&L: ${pos.unrealizedPnl}`);
2615
+ });
2616
+ ```
2617
+
2618
+ ### `prepareBuyOrder(gameId: string, outcomeId: string, amountMinor: number): Promise<{ transaction: string }>`
2619
+
2620
+ Prepare a buy order deposit transaction (unsigned, for client signing).
2621
+
2622
+ ```typescript
2623
+ const { transaction } = await sdk.markets.prepareBuyOrder(
2624
+ 'game-id',
2625
+ 'player-a-id',
2626
+ 1_000_000,
2627
+ );
2628
+ // ... sign transaction with wallet ...
2629
+ ```
2630
+
2631
+ ### `submitBuyOrder(gameId: string, signedTransaction: string, outcomeId: string, amountMinor: number): Promise<MarketBuyResult>`
2632
+
2633
+ Submit a signed buy order and execute the trade. Shares are received instantly via the AMM pool.
2634
+
2635
+ ```typescript
2636
+ const result = await sdk.markets.submitBuyOrder(
2637
+ 'game-id',
2638
+ signedTxBase64,
2639
+ 'player-a-id',
2640
+ 1_000_000,
2641
+ );
2642
+ console.log(
2643
+ `Received ${result.sharesReceived} shares at ${result.costPerShare} per share`,
2644
+ );
2645
+ console.log('New prices:', result.newPrices);
2646
+ ```
2647
+
2648
+ **Parameters:**
2649
+
2650
+ - `gameId: string` - The game ID
2651
+ - `signedTransaction: string` - Base64-encoded signed Solana transaction
2652
+ - `outcomeId: string` - The outcome to buy (typically a player ID)
2653
+ - `amountMinor: number` - Amount to spend in USDC minor units
2654
+
2655
+ ### `sellShares(gameId: string, outcomeId: string, shares: number): Promise<MarketSellResult>`
2656
+
2657
+ Sell shares back to the AMM pool. Returns USDC based on current pool state.
2658
+
2659
+ ```typescript
2660
+ const result = await sdk.markets.sellShares('game-id', 'player-a-id', 500);
2661
+ console.log(
2662
+ `Received ${result.amountReceived} USDC at ${result.pricePerShare} per share`,
2663
+ );
2664
+ console.log('New prices:', result.newPrices);
2665
+ ```
2666
+
2667
+ **Parameters:**
2668
+
2669
+ - `gameId: string` - The game ID
2670
+ - `outcomeId: string` - The outcome to sell
2671
+ - `shares: number` - Number of shares to sell (in minor units)
2672
+
2673
+ ### `redeemShares(gameId: string): Promise<RedeemResult>`
2674
+
2675
+ Redeem winning shares after a market is resolved. Returns payout, fee, net payout, and profit.
2676
+
2677
+ ```typescript
2678
+ const result = await sdk.markets.redeemShares('game-id');
2679
+ console.log(`Payout: ${result.payout}`);
2680
+ console.log(`Fee: ${result.fee}`);
2681
+ console.log(`Net payout: ${result.netPayout}`);
2682
+ console.log(`Profit: ${result.profit}`);
2683
+ ```
2684
+
2685
+ ### Admin Methods
2686
+
2687
+ #### `getAdminStats(): Promise<AdminMarketStats>`
2688
+
2689
+ Get aggregate prediction market statistics. Admin only.
2690
+
2691
+ ```typescript
2692
+ const stats = await sdk.markets.getAdminStats();
2693
+ console.log(`Total markets: ${stats.totalMarkets}`);
2694
+ console.log(`Open: ${stats.openMarkets}, Resolved: ${stats.resolvedMarkets}`);
2695
+ console.log(`Total volume: ${stats.totalVolume}`);
2696
+ console.log(`Unique traders: ${stats.uniqueTraders}`);
2697
+ ```
2698
+
2699
+ #### `getAdminDailyStats(days?: number): Promise<AdminMarketDailyStats>`
2700
+
2701
+ Get daily prediction market stats breakdown. Admin only. Defaults to 30 days.
2702
+
2703
+ ```typescript
2704
+ const daily = await sdk.markets.getAdminDailyStats(7);
2705
+ daily.days.forEach((day) => {
2706
+ console.log(
2707
+ `${day.date}: ${day.tradesExecuted} trades, volume ${day.volume}`,
2708
+ );
2709
+ });
2710
+ ```
2711
+
2712
+ #### `getAdminMarkets(page?: number, limit?: number): Promise<{ markets: AdminMarketDetail[]; total: number }>`
2713
+
2714
+ List all prediction markets with order and position counts. Admin only.
2715
+
2716
+ ```typescript
2717
+ const { markets, total } = await sdk.markets.getAdminMarkets(1, 20);
2718
+ console.log(`${total} total markets`);
2719
+ markets.forEach((m) => {
2720
+ console.log(`${m.id}: ${m.status}, ${m.positionCount} positions`);
2721
+ });
2722
+ ```
2723
+
2724
+ ### Prediction Market Types
2725
+
2726
+ ```typescript
2727
+ interface MarketState {
2728
+ marketId: string;
2729
+ gameId: string;
2730
+ status: string;
2731
+ outcomes: string[];
2732
+ prices: Record<string, number>;
2733
+ totalCollateral: string;
2734
+ totalVolume: string;
2735
+ totalFees: string;
2736
+ resolvedOutcome: string | null;
2737
+ }
2738
+
2739
+ interface MarketPosition {
2740
+ outcomeId: string;
2741
+ shares: string;
2742
+ avgCostPerShare: string;
2743
+ totalCost: string;
2744
+ currentPrice: number;
2745
+ currentValue: string;
2746
+ unrealizedPnl: string;
2747
+ }
2748
+
2749
+ interface MarketBuyResult {
2750
+ tradeId: string;
2751
+ sharesReceived: string;
2752
+ costPerShare: string;
2753
+ newPrices: Record<string, number>;
2754
+ }
2755
+
2756
+ interface MarketSellResult {
2757
+ tradeId: string;
2758
+ amountReceived: string;
2759
+ pricePerShare: string;
2760
+ newPrices: Record<string, number>;
2761
+ }
2762
+
2763
+ interface RedeemResult {
2764
+ status: string;
2765
+ payout: string;
2766
+ fee: string;
2767
+ netPayout: string;
2768
+ profit: string;
2769
+ }
2770
+
2771
+ interface AdminMarketStats {
2772
+ totalMarkets: number;
2773
+ openMarkets: number;
2774
+ resolvedMarkets: number;
2775
+ totalVolume: string;
2776
+ totalFees: string;
2777
+ uniqueTraders: number;
2778
+ }
2779
+
2780
+ interface AdminMarketDailyStats {
2781
+ days: AdminMarketDailyStatsItem[];
2782
+ }
2783
+
2784
+ interface AdminMarketDailyStatsItem {
2785
+ date: string;
2786
+ marketsOpened: number;
2787
+ marketsResolved: number;
2788
+ tradesExecuted: number;
2789
+ volume: string;
2790
+ fees: string;
2791
+ }
2792
+
2793
+ interface AdminMarketDetail {
2794
+ id: string;
2795
+ gameId: string;
2796
+ status: string;
2797
+ outcomes: string[];
2798
+ resolvedOutcome: string | null;
2799
+ totalCollateral: string;
2800
+ totalVolume: string;
2801
+ totalFees: string;
2802
+ createdAt: string;
2803
+ resolvedAt: string | null;
2804
+ positionCount: number;
2805
+ }
2806
+ ```
2807
+
2808
+ ### Complete Prediction Market Flow Example
2809
+
2810
+ ```typescript
2811
+ import { SDK, BrowserLocalStorage } from '@dimcool/sdk';
2812
+
2813
+ const sdk = new SDK({
2814
+ appId: 'dim',
2815
+ baseUrl: 'https://api.example.com',
2816
+ storage: new BrowserLocalStorage(),
2817
+ });
2818
+
2819
+ // 1. Authenticate
2820
+ await sdk.auth.login('user@example.com', 'password');
2821
+
2822
+ // 2. Get market state for a game
2823
+ const market = await sdk.markets.getMarket('game-id');
2824
+ console.log('Prices:', market.prices);
2825
+ console.log('Volume:', market.totalVolume);
2826
+
2827
+ // 3. Buy shares of an outcome (two-step: prepare + sign + submit)
2828
+ const { transaction } = await sdk.markets.prepareBuyOrder(
2829
+ 'game-id',
2830
+ 'player-a-id',
2831
+ 1_000_000,
2832
+ );
2833
+ // ... sign transaction with wallet ...
2834
+ const buyResult = await sdk.markets.submitBuyOrder(
2835
+ 'game-id',
2836
+ signedTxBase64,
2837
+ 'player-a-id',
2838
+ 1_000_000,
2839
+ );
2840
+ console.log(`Bought: ${buyResult.sharesReceived} shares`);
2841
+
2842
+ // 4. Check positions
2843
+ const positions = await sdk.markets.getMyPositions('game-id');
2844
+ positions.forEach((pos) => {
2845
+ console.log(
2846
+ `${pos.outcomeId}: ${pos.shares} shares, P&L: ${pos.unrealizedPnl}`,
2847
+ );
2848
+ });
2849
+
2850
+ // 5. Sell shares back to the AMM pool
2851
+ const sellResult = await sdk.markets.sellShares('game-id', 'player-a-id', 250);
2852
+ console.log(`Received: ${sellResult.amountReceived} USDC`);
2853
+
2854
+ // 6. After game resolves, redeem winning shares
2855
+ const redeem = await sdk.markets.redeemShares('game-id');
2856
+ console.log(`Net payout: ${redeem.netPayout}, Profit: ${redeem.profit}`);
2857
+ ```
2858
+
2859
+ ---
2860
+
2861
+ ## Realtime Transport & Stores
2862
+
2863
+ The SDK now exposes a transport abstraction and Zustand vanilla stores for real-time
2864
+ state. This is the preferred API for new integrations.
2865
+
2866
+ ### Transport (`sdk.wsTransport`)
2867
+
2868
+ The transport owns the realtime connection. Use `SharedWorkerTransport` in browsers
2869
+ or `StandaloneWsTransport` in Node/unsupported environments.
2870
+
2871
+ ```typescript
2872
+ import { SharedWorkerTransport } from '@dimcool/sdk';
2873
+
2874
+ const workerUrl = new URL('path/to/shared-worker.js', import.meta.url);
2875
+ const wsTransport = new SharedWorkerTransport(workerUrl.toString(), baseUrl);
2876
+
2877
+ const sdk = new SDK({
2878
+ appId: 'dim',
2879
+ baseUrl,
2880
+ storage,
2881
+ wsTransport,
2882
+ });
2883
+ ```
2884
+
2885
+ ### Stores
2886
+
2887
+ - `sdk.lobbyStore`, `sdk.gameStore`, `sdk.gameActionsStore`
2888
+ - `sdk.chatStore`, `sdk.dmThreadsStore`, `sdk.notificationsStore`
2889
+
2890
+ These stores are updated by the `WsRouter` and are the single source of truth for
2891
+ realtime UI. Room membership is ref-counted and mapped to backend join/leave emits.
2892
+
2893
+ ### Event Bus (low-level)
2894
+
2895
+ If you need raw event streams, use `sdk.events.subscribe(eventName, handler)`.
2896
+
2897
+ ## Type Definitions
2898
+
2899
+ The SDK exports TypeScript types for all API responses:
2900
+
2901
+ ```typescript
2902
+ import type {
2903
+ User,
2904
+ PublicUser,
2905
+ PaginatedUsers,
2906
+ PaginatedFriends,
2907
+ PaginatedSearchUsers,
2908
+ FeatureFlag,
2909
+ LoginResponse,
2910
+ GenerateHandshakeResponse,
2911
+ UsernameAvailabilityResponse,
2912
+ Lobby,
2913
+ LobbyPlayer,
2914
+ QueueStats,
2915
+ GameType,
2916
+ Game,
2917
+ GamePlayer,
2918
+ MarketState,
2919
+ MarketPosition,
2920
+ MarketOrderResult,
2921
+ RedeemResult,
2922
+ AdminMarketStats,
2923
+ AdminMarketDailyStats,
2924
+ AdminMarketDetail,
2925
+ WebSocketEventMap,
2926
+ ConnectionState,
2927
+ } from '@dimcool/sdk';
2928
+ ```
2929
+
2930
+ ---
2931
+
2932
+ ## Error Handling
2933
+
2934
+ The SDK throws errors for failed API requests. Always wrap SDK calls in try-catch blocks:
2935
+
2936
+ ```typescript
2937
+ try {
2938
+ await sdk.auth.login('user@example.com', 'password');
2939
+ } catch (error) {
2940
+ if (error instanceof Error) {
2941
+ console.error('Login failed:', error.message);
2942
+ }
2943
+ }
2944
+ ```
2945
+
2946
+ ---
2947
+
2948
+ ## Examples
2949
+
2950
+ ### Complete Authentication Flow
2951
+
2952
+ ```typescript
2953
+ import { SDK, BrowserLocalStorage } from '@dimcool/sdk';
2954
+
2955
+ const sdk = new SDK({
2956
+ appId: 'dim',
2957
+ baseUrl: 'https://api.example.com',
2958
+ storage: new BrowserLocalStorage(),
2959
+ });
2960
+
2961
+ // Login
2962
+ try {
2963
+ const response = await sdk.auth.login('user@example.com', 'password');
2964
+ console.log('Logged in as:', response.user.email);
2965
+ } catch (error) {
2966
+ console.error('Login failed:', error);
2967
+ }
2968
+
2969
+ // Check authentication
2970
+ if (sdk.auth.isAuthenticated()) {
2971
+ // Fetch user data
2972
+ const user = await sdk.users.getUserById(response.user.id);
2973
+ }
2974
+
2975
+ // Logout
2976
+ sdk.auth.logout();
2977
+ ```
2978
+
2979
+ ### Lobby Management
2980
+
2981
+ ```typescript
2982
+ // Create a lobby
2983
+ const lobby = await sdk.lobbies.createLobby('rock-paper-scissors');
2984
+
2985
+ // Invite a friend
2986
+ await sdk.lobbies.inviteFriend(lobby.id, 'friend-user-id');
2987
+
2988
+ // Get lobby status
2989
+ const updatedLobby = await sdk.lobbies.getLobby(lobby.id);
2990
+
2991
+ // Leave lobby
2992
+ await sdk.lobbies.leaveLobby(lobby.id);
2993
+ ```
2994
+
2995
+ ### Feature Flag Checking
2996
+
2997
+ ```typescript
2998
+ // Load feature flags
2999
+ await sdk.featureFlags.getFeatureFlags();
3000
+
3001
+ // Check if feature is enabled
3002
+ if (sdk.featureFlags.isEnabledFlag('enable-rock-paper-scissors')) {
3003
+ // Show game UI
3004
+ }
3005
+ ```
3006
+
3007
+ ### Getting Available Games
3008
+
3009
+ ```typescript
3010
+ // Get available game types (filtered by feature flags)
3011
+ const games = await sdk.games.getAvailableGames();
3012
+
3013
+ // Display games to user
3014
+ games.forEach((game) => {
3015
+ console.log(`${game.name}: ${game.minPlayers}-${game.maxPlayers} players`);
3016
+ });
3017
+
3018
+ // Create lobby with validated game type
3019
+ if (games.length > 0) {
3020
+ const lobby = await sdk.lobbies.createLobby(games[0].id);
3021
+ }
3022
+ ```
3023
+
3024
+ ---
3025
+
3026
+ ## License
3027
+
3028
+ MIT