@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 +3028 -0
- package/dist/index.cjs +25657 -0
- package/dist/index.d.cts +2629 -0
- package/dist/index.d.ts +2629 -0
- package/dist/index.js +25618 -0
- package/package.json +48 -0
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
|