@suigar/sdk 2.0.0-beta.1 → 2.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,31 +5,75 @@ TypeScript SDK for building Suigar v2 game transactions on Sui.
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @suigar/sdk
8
+ npm install --save @suigar/sdk @mysten/sui @mysten/bcs
9
9
  ```
10
10
 
11
11
  Runtime requirements:
12
12
 
13
13
  - Node.js `>=22`
14
- - `@mysten/sui`
14
+ - ESM project configuration (`"type": "module"`)
15
+ - `@mysten/sui` v2
16
+ - `@mysten/bcs` v2
17
+
18
+ This SDK targets Sui TypeScript SDK 2.0+ only. Follow the official [Sui 2.0 migration guide](https://sdk.mystenlabs.com/sui/migrations/sui-2.0) if your app still uses the pre-2.0 client API.
15
19
 
16
20
  ## What This Package Exposes
17
21
 
18
- The package root currently exposes the extension factory:
22
+ The package ships three public entrypoints:
23
+
24
+ - `@suigar/sdk` for the extension factory and runtime client class
25
+ - `@suigar/sdk/games` for game-specific public types
26
+ - `@suigar/sdk/utils` for public parser, constants, and numeric helpers
27
+
28
+ The package root exposes the extension factory and client class:
19
29
 
20
30
  ```ts
21
- import { suigar } from '@suigar/sdk';
31
+ import { suigar, SuigarClient } from '@suigar/sdk';
22
32
  ```
23
33
 
24
34
  It does not export the individual transaction builders from the package root.
25
- It also does not export `SuigarClient` as a public root symbol.
35
+ Those stay on the registered extension instance under `client.suigar.tx`.
36
+
37
+ Utility exports are available from the utils subpath:
38
+
39
+ ```ts
40
+ import {
41
+ DEFAULT_GAS_BUDGET_MIST,
42
+ DEFAULT_LIMBO_MULTIPLIER_SCALE,
43
+ DEFAULT_RANGE_SCALE,
44
+ fromMoveFloat,
45
+ fromMoveI64,
46
+ parseCoinType,
47
+ parseGameDetails,
48
+ RANGE_POINT_LIMIT,
49
+ toBigInt,
50
+ toU8,
51
+ } from '@suigar/sdk/utils';
52
+ ```
53
+
54
+ Game-specific type exports are available from the dedicated `games` subpath:
55
+
56
+ ```ts
57
+ import type {
58
+ BuildCoinflipTransactionOptions,
59
+ BuildCreatePvPCoinflipTransactionOptions,
60
+ CoinSide,
61
+ PvPCoinflipAction,
62
+ } from '@suigar/sdk/games';
63
+ ```
64
+
65
+ Current game-type subpath exports:
66
+
67
+ - `@suigar/sdk/games`: `CoinSide`, `PvPCoinflipAction`, `BuildCoinflipTransactionOptions`, `BuildLimboTransactionOptions`, `BuildPlinkoTransactionOptions`, `BuildRangeTransactionOptions`, `BuildWheelTransactionOptions`, `BuildCreatePvPCoinflipTransactionOptions`, `BuildJoinPvPCoinflipTransactionOptions`, `BuildCancelPvPCoinflipTransactionOptions`
26
68
 
27
69
  What you actually use at runtime is the registered extension instance:
28
70
 
29
71
  ```ts
30
- const client = new SuiClient({ url }).$extend(suigar());
72
+ const client = new SuiGrpcClient({ baseUrl, network }).$extend(suigar());
31
73
 
32
74
  client.suigar.serializeTransactionToBase64(...);
75
+ client.suigar.getConfig();
76
+ client.suigar.getPvPCoinflipGames(...);
33
77
  client.suigar.bcs;
34
78
  client.suigar.tx;
35
79
  ```
@@ -37,22 +81,16 @@ client.suigar.tx;
37
81
  ## Quick Start
38
82
 
39
83
  ```ts
40
- import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
84
+ import { SuiGrpcClient } from '@mysten/sui/grpc';
41
85
  import { suigar } from '@suigar/sdk';
42
86
 
43
- const client = new SuiClient({
44
- url: getFullnodeUrl('testnet'),
45
- }).$extend(
46
- suigar({
47
- pyth: {
48
- suiPriceInfoObjectId: '0xPYTH_SUI_PRICE_INFO',
49
- usdcPriceInfoObjectId: '0xPYTH_USDC_PRICE_INFO',
50
- },
51
- }),
52
- );
87
+ const client = new SuiGrpcClient({
88
+ baseUrl: 'https://fullnode.testnet.sui.io:443',
89
+ network: 'testnet',
90
+ }).$extend(suigar());
53
91
 
54
92
  const tx = client.suigar.tx.createBetTransaction('coinflip', {
55
- owner: '0x123',
93
+ playerAddress: '0x123',
56
94
  coinType: '0x2::sui::SUI',
57
95
  stake: 1_000_000_000n,
58
96
  side: 'heads',
@@ -67,78 +105,83 @@ const base64 = await client.suigar.serializeTransactionToBase64(tx);
67
105
 
68
106
  Creates a named Sui client extension. By default, it registers under `client.suigar`.
69
107
 
108
+ ### Partner Setup
109
+
110
+ > **Important:** `partner` is the partner wallet address. Configure it once
111
+ > when you register the extension so the SDK can prepend that wallet address to
112
+ > supported bet metadata automatically.
113
+
70
114
  ```ts
71
- const client = new SuiClient({ url }).$extend(suigar());
115
+ const client = new SuiGrpcClient({ baseUrl, network }).$extend(
116
+ suigar({ partner: '0xpartner_wallet_address' }),
117
+ );
72
118
 
73
119
  client.suigar;
74
120
  ```
75
121
 
122
+ Do not pass a partner slug, label, or display name here. Use the wallet
123
+ address that should receive partner attribution onchain.
124
+
76
125
  You can rename the extension:
77
126
 
78
127
  ```ts
79
- const client = new SuiClient({ url }).$extend(suigar({ name: 'casino' }));
128
+ const client = new SuiGrpcClient({ baseUrl, network }).$extend(
129
+ suigar({ name: 'games' }),
130
+ );
80
131
 
81
- client.casino.tx;
82
- client.casino.bcs;
132
+ client.games.tx;
133
+ client.games.bcs;
83
134
  ```
84
135
 
85
136
  ## Config
86
137
 
87
138
  `suigar(options?)` resolves config from:
88
139
 
89
- - the connected Sui network
90
- - internal default package ids
91
- - internal default SweetHouse package id by network
92
- - default coin types for `SUI`, `USDC`, and FlowX `USDC`
93
- - user overrides
140
+ - internal package ids by network
141
+ - internal supported coin types by network
142
+ - internal price info object ids by network
143
+ - the connected client network
144
+ - the extension name
94
145
 
95
146
  Supported override areas:
96
147
 
97
148
  - `name`
98
- - `sweetHousePackageId`
99
- - `coinTypes.sui`
100
- - `coinTypes.usdc`
101
- - `coinTypes.usdcFlowx`
102
- - `gamesPackageId.coinflip`
103
- - `gamesPackageId.limbo`
104
- - `gamesPackageId.plinko`
105
- - `gamesPackageId['pvp-coinflip']`
106
- - `gamesPackageId.range`
107
- - `gamesPackageId.wheel`
108
- - `pyth.packageId`
109
- - `pyth.suiPriceInfoObjectId`
110
- - `pyth.usdcPriceInfoObjectId`
111
- - `pyth.priceInfoObjectIds[coinType]`
112
-
113
- Example:
149
+ - `partner`
114
150
 
115
- ```ts
116
- const client = new SuiClient({ url }).$extend(
117
- suigar({
118
- sweetHousePackageId: '0xsweethouse',
119
- pyth: {
120
- suiPriceInfoObjectId: '0xsui',
121
- usdcPriceInfoObjectId: '0xusdc',
122
- priceInfoObjectIds: {
123
- '0x123::custom::TOKEN': '0xprice',
124
- },
125
- },
126
- gamesPackageId: {
127
- coinflip: '0xcoinflip',
128
- wheel: '0xwheel',
129
- },
130
- }),
131
- );
132
- ```
151
+ If `partner` is configured, the SDK automatically writes that partner wallet
152
+ address into the onchain metadata vec-map. Transaction builder options may also
153
+ include `metadata`, but reserved keys such as `partner` and `referrer` are
154
+ ignored with a warning when provided manually.
133
155
 
134
156
  ## Runtime Surface
135
157
 
136
- The registered extension instance exposes three main areas:
158
+ The registered extension instance exposes the main runtime surface:
137
159
 
160
+ - `getConfig()`
138
161
  - `serializeTransactionToBase64(transaction, options?)`
162
+ - `getPvPCoinflipGames(options?)`
139
163
  - `bcs`
140
164
  - `tx`
141
165
 
166
+ ### `getConfig()`
167
+
168
+ Returns the resolved SDK configuration for the connected network.
169
+
170
+ This is intended mainly for debugging and inspection, for example to verify the
171
+ resolved package ids or supported coin mappings for the active client network.
172
+
173
+ It includes:
174
+
175
+ - `packageIds`
176
+ - `registryIds`
177
+ - `coinTypes`
178
+ - `priceInfoObjectIds`
179
+
180
+ ```ts
181
+ const config = client.suigar.getConfig();
182
+ console.log(config.packageIds);
183
+ ```
184
+
142
185
  ### `serializeTransactionToBase64(transaction, options?)`
143
186
 
144
187
  Builds a transaction with the configured Sui client and returns base64-encoded transaction bytes.
@@ -149,6 +192,42 @@ Use this when you need a transport-safe payload for a wallet, API, or external s
149
192
  const base64 = await client.suigar.serializeTransactionToBase64(tx);
150
193
  ```
151
194
 
195
+ ### `getPvPCoinflipGames(options?)`
196
+
197
+ Lists unresolved PvP coinflip games from the configured PvP registry.
198
+
199
+ This reads the registry dynamic fields for the active network and resolves each
200
+ entry into parsed game state through a bulk `client.core.getObjects()` lookup.
201
+ Registry membership is the unresolved-state signal: once a match is joined and
202
+ resolved, the Move flow removes it from the registry and deletes the live
203
+ `Game` object.
204
+
205
+ Use this when a product needs the current set of open PvP coinflip matches for
206
+ browsing or lobby views.
207
+
208
+ By default, per-object fetch or parse failures are skipped so one broken or
209
+ already-deleted registry entry does not reject the full lookup. Pass
210
+ `throwOnError: true` if you want the call to reject instead.
211
+
212
+ Any supported `listDynamicFields()` options such as `limit`, `cursor`, or
213
+ `signal` can be passed through `options`.
214
+
215
+ ```ts
216
+ const games = await client.suigar.getPvPCoinflipGames({ limit: 20 });
217
+
218
+ for (const game of games) {
219
+ console.log(game.id);
220
+ console.log(game.coinType);
221
+ }
222
+ ```
223
+
224
+ ```ts
225
+ const games = await client.suigar.getPvPCoinflipGames({
226
+ limit: 20,
227
+ throwOnError: true,
228
+ });
229
+ ```
230
+
152
231
  ## `tx`
153
232
 
154
233
  Transaction builders live under `client.suigar.tx`.
@@ -165,7 +244,7 @@ Use `createBetTransaction(gameId, options)` for:
165
244
 
166
245
  ```ts
167
246
  const tx = client.suigar.tx.createBetTransaction('coinflip', {
168
- owner: '0x123',
247
+ playerAddress: '0x123',
169
248
  coinType: '0x2::sui::SUI',
170
249
  stake: 1_000_000_000n,
171
250
  side: 'tails',
@@ -174,14 +253,13 @@ const tx = client.suigar.tx.createBetTransaction('coinflip', {
174
253
 
175
254
  Shared option shape:
176
255
 
177
- - `owner: string`
256
+ - `playerAddress: string`
178
257
  - `coinType: string`
179
258
  - `stake: number | bigint`
180
259
  - `cashStake?: number | bigint`
181
260
  - `betCount?: number | bigint`
182
261
  - `metadata?: Record<string, string | number | boolean | bigint | Uint8Array | number[] | null | undefined>`
183
262
  - `gasBudget?: number | bigint`
184
- - `sender?: string`
185
263
  - `allowGasCoinShortcut?: boolean`
186
264
 
187
265
  Shared behavior:
@@ -189,10 +267,11 @@ Shared behavior:
189
267
  - `stake` is the logical stake passed into the Move call
190
268
  - `cashStake` controls the withdrawn balance and defaults to `stake`
191
269
  - `betCount` defaults to `1`
192
- - `sender` overrides the transaction sender
193
270
  - `metadata` is encoded into `keys` and `values` byte arrays
194
- - the SDK resolves the Pyth price info object from the configured coin mapping
195
- - the reward object is transferred back to `owner`
271
+ - `partner` configured via `suigar({ partner })` is prepended automatically to metadata as the partner wallet address
272
+ - `metadata.partner` and `metadata.referrer` are reserved and ignored with a warning
273
+ - the SDK resolves the price info object from the configured supported-coin mapping
274
+ - the reward object is transferred back to `playerAddress`
196
275
 
197
276
  Per-game options:
198
277
 
@@ -206,27 +285,35 @@ Examples:
206
285
 
207
286
  ```ts
208
287
  const limboTx = client.suigar.tx.createBetTransaction('limbo', {
209
- owner: '0x123',
288
+ playerAddress: '0x123',
210
289
  coinType: '0x2::sui::SUI',
211
290
  stake: 1_000_000_000n,
212
291
  targetMultiplier: 2.5,
213
292
  });
214
293
 
215
294
  const rangeTx = client.suigar.tx.createBetTransaction('range', {
216
- owner: '0x123',
295
+ playerAddress: '0x123',
217
296
  coinType: '0x2::sui::SUI',
218
297
  stake: 1_000_000_000n,
219
- leftPoint: 0.95,
220
- rightPoint: 1.05,
298
+ leftPoint: 25,
299
+ rightPoint: 75,
221
300
  outOfRange: false,
222
301
  });
223
302
  ```
224
303
 
225
- Notes:
304
+ > **Note:**
305
+ >
306
+ > - limbo converts `targetMultiplier` with `Math.round(targetMultiplier * scale)`
307
+ > - with the default limbo scale `100`, exposed as `DEFAULT_LIMBO_MULTIPLIER_SCALE`, a target multiplier of `2.5` becomes `250` onchain
308
+ > - range converts each point with `Math.round(value * scale)`
309
+ > - range points are bounded by the contract limit exposed as `RANGE_POINT_LIMIT`
310
+ > - with the default range scale `1_000_000`, exposed as `DEFAULT_RANGE_SCALE`, valid UI values are `0` to `100`
311
+ > - plinko and wheel `configId` must fit in `u8`
226
312
 
227
- - limbo converts `targetMultiplier` with `Math.round(targetMultiplier * scale)`
228
- - range converts each point with `Math.round(value * scale)`
229
- - plinko and wheel `configId` must fit in `u8`
313
+ > **Tip:**
314
+ >
315
+ > - if you set `scale` to `10_000_000`, valid UI values become `0` to `10`
316
+ > - do not pre-scale range points before passing them to the SDK; pass the human value and let the SDK scale it once
230
317
 
231
318
  ### PvP Coinflip
232
319
 
@@ -240,7 +327,7 @@ Create:
240
327
 
241
328
  ```ts
242
329
  const tx = client.suigar.tx.createPvPCoinflipTransaction('create', {
243
- owner: '0x123',
330
+ playerAddress: '0x123',
244
331
  coinType: '0x2::sui::SUI',
245
332
  stake: 1_000_000_000n,
246
333
  side: 'heads',
@@ -252,11 +339,9 @@ Join:
252
339
 
253
340
  ```ts
254
341
  const tx = client.suigar.tx.createPvPCoinflipTransaction('join', {
255
- owner: '0x123',
342
+ playerAddress: '0x123',
256
343
  coinType: '0x2::sui::SUI',
257
344
  gameId: '0xGAME_ID',
258
- extraObjectId: '0xEXTRA_OBJECT_ID',
259
- stake: 1_000_000_000n,
260
345
  });
261
346
  ```
262
347
 
@@ -264,25 +349,27 @@ Cancel:
264
349
 
265
350
  ```ts
266
351
  const tx = client.suigar.tx.createPvPCoinflipTransaction('cancel', {
267
- owner: '0x123',
352
+ playerAddress: '0x123',
268
353
  coinType: '0x2::sui::SUI',
269
354
  gameId: '0xGAME_ID',
270
355
  });
271
356
  ```
272
357
 
358
+ Join derives the stake from `gameId` and uses the configured price info object
359
+ id for `coinType`.
360
+
273
361
  PvP shared options:
274
362
 
275
- - `owner: string`
363
+ - `playerAddress: string`
276
364
  - `coinType: string`
277
- - `metadata?: ...`
365
+ - `metadata?: Record<string, string | number | boolean | bigint | Uint8Array | number[] | null | undefined>`
278
366
  - `gasBudget?: number | bigint`
279
- - `sender?: string`
280
367
  - `allowGasCoinShortcut?: boolean`
281
368
 
282
369
  Action-specific options:
283
370
 
284
371
  - `create`: `stake`, `side`, `isPrivate?`
285
- - `join`: `gameId`, `extraObjectId`, `stake`
372
+ - `join`: `gameId`
286
373
  - `cancel`: `gameId`
287
374
 
288
375
  ## `bcs`
@@ -291,24 +378,70 @@ BCS helpers live under `client.suigar.bcs`.
291
378
 
292
379
  Current exposed helpers:
293
380
 
381
+ - `PvPCoinflipGame`
294
382
  - `BetResultEvent`
295
- - `PvPCoinflipGameCreated`
296
- - `PvPCoinflipGameResolved`
297
- - `PvPCoinflipGameCancelled`
383
+ - `PvPCoinflipGameCreatedEvent`
384
+ - `PvPCoinflipGameResolvedEvent`
385
+ - `PvPCoinflipGameCancelledEvent`
298
386
 
299
- These are generated Move struct/event decoders. Use them to parse Suigar event payloads and structured onchain content.
387
+ These are generated Move event decoders. Use them to parse Suigar event payloads from transaction results. The `@suigar/sdk/utils` subpath also exposes parser helpers for generated BCS values:
388
+
389
+ - `PvPCoinflipGame` parses a PvP coinflip game object's `content`
390
+ - `fromMoveI64(float.exp)` converts a generated Move `i64` exponent to a JavaScript number
391
+ - `fromMoveFloat(float)` converts a generated Move `Float` struct to a JavaScript number
392
+ - `parseCoinType(type)` extracts the normalized coin type from generic Move object type strings such as PvP coinflip `Game<T>`
393
+ - `parseGameDetails(game_details)` decodes `BetResultEvent.game_details` entries into the expected string, number, and boolean values
394
+
395
+ ### Parse PvP Coinflip Game Object Data
396
+
397
+ Use the generated BCS helper when you want to fetch and parse a game object:
398
+
399
+ ```ts
400
+ const game = await client.suigar.bcs.PvPCoinflipGame.get({
401
+ client,
402
+ objectId: '0xGAME_ID',
403
+ });
404
+
405
+ console.log(game.json);
406
+ ```
300
407
 
301
408
  ### Parse Standard Bet Result Data
302
409
 
303
410
  ```ts
304
- const { object } = await client.core.getObject({
305
- objectId: '0xOBJECT_ID',
411
+ const executeResult = await client.core.executeTransaction({
412
+ transaction: transactionBytes,
413
+ signatures: [signature],
306
414
  include: {
307
- content: true,
415
+ events: true,
308
416
  },
309
417
  });
310
418
 
311
- const decoded = client.suigar.bcs.BetResultEvent.parse(object.content);
419
+ const finalResult = await client.core.waitForTransaction({
420
+ result: executeResult,
421
+ include: {
422
+ effects: true,
423
+ events: true,
424
+ },
425
+ });
426
+
427
+ if (finalResult.$kind === 'FailedTransaction') {
428
+ throw new Error(finalResult.FailedTransaction.status.error?.message);
429
+ }
430
+
431
+ console.log(finalResult.Transaction.digest);
432
+
433
+ const transactionResult = finalResult.Transaction;
434
+
435
+ const betResults = [];
436
+
437
+ for (const event of transactionResult.events ?? []) {
438
+ try {
439
+ const decoded = client.suigar.bcs.BetResultEvent.parse(event.bcs);
440
+ betResults.push(decoded);
441
+ } catch {
442
+ // Ignore non-BetResultEvent payloads.
443
+ }
444
+ }
312
445
  ```
313
446
 
314
447
  Parsed fields include:
@@ -322,36 +455,68 @@ Parsed fields include:
322
455
  - `game_details`
323
456
  - `metadata`
324
457
 
325
- `game_details` and `metadata` decode as `VecMap<string, vector<u8>>`-shaped data, so values come back as byte arrays.
458
+ `game_details` and `metadata` decode as `VecMap<string, vector<u8>>`-shaped data, so values come back as byte arrays. Use `parseGameDetails` from `@suigar/sdk/utils` to decode `game_details` with the SDK's known game-detail schemas.
326
459
 
327
460
  ```ts
328
- const textDecoder = new TextDecoder();
461
+ import { parseGameDetails } from '@suigar/sdk/utils';
329
462
 
330
- const metadata = new Map(
331
- decoded.metadata.contents.map(({ key, value }) => [
332
- key,
333
- textDecoder.decode(new Uint8Array(value)),
334
- ]),
335
- );
463
+ const decoded = client.suigar.bcs.BetResultEvent.parse(event.bcs);
464
+ const gameDetails = parseGameDetails(decoded.game_details);
336
465
  ```
337
466
 
338
- Important:
467
+ `parseGameDetails` preserves the onchain keys and only changes the value representation. For example, coinflip details keep keys such as `player_bet` and `coin_outcome`; range details keep keys such as `roll_value`, `win`, and `payout_multiplier`.
468
+
469
+ When the extension is configured with `partner`, decoded event `metadata` will
470
+ contain that partner wallet address under the `partner` entry.
339
471
 
340
- - use `content`, not `objectBcs`, with these generated parsers
341
- - the generated parser expects the struct payload, not a full object envelope
472
+ > **Important:**
473
+ >
474
+ > - execute or wait for the transaction with `include: { events: true }`
475
+ > - unwrap the core API union with `result.$kind`, `result.Transaction`, and `result.FailedTransaction`
476
+ > - parse emitted events from the unwrapped transaction result
477
+ > - use `event.bcs` for consistent decoding across transports
478
+ > - use `parseGameDetails(decoded.game_details)` instead of hand-decoding standard game detail byte arrays
479
+
480
+ > **Tip:**
481
+ >
482
+ > - `waitForTransaction({ result, include: { effects: true, events: true } })` is useful when you want the finalized transaction result before decoding
483
+ > - these helpers decode the event payload itself, not a full transaction response
342
484
 
343
485
  ### Parse PvP Coinflip Event Data
344
486
 
345
- Use the matching helper for the PvP coinflip event payload you fetched from chain:
487
+ Use the matching helper for each PvP coinflip event payload found in `transactionResult.events`:
346
488
 
347
- - `client.suigar.bcs.PvPCoinflipGameCreated`
348
- - `client.suigar.bcs.PvPCoinflipGameResolved`
349
- - `client.suigar.bcs.PvPCoinflipGameCancelled`
489
+ - `client.suigar.bcs.PvPCoinflipGameCreatedEvent`
490
+ - `client.suigar.bcs.PvPCoinflipGameResolvedEvent`
491
+ - `client.suigar.bcs.PvPCoinflipGameCancelledEvent`
350
492
 
351
493
  ## Development
352
494
 
353
495
  ```bash
354
- npm run build
355
- npm run typecheck
356
- npm test
496
+ pnpm --dir packages/sdk build
497
+ pnpm --dir packages/sdk typecheck
498
+ pnpm --dir packages/sdk test
499
+ ```
500
+
501
+ ## Example App
502
+
503
+ This repository includes a Next.js integration playground in [apps/playground](../../apps/playground).
504
+
505
+ It demonstrates:
506
+
507
+ - standard game transactions through `client.suigar.tx.createBetTransaction(...)`
508
+ - PvP coinflip create, join, and cancel flows through `client.suigar.tx.createPvPCoinflipTransaction(...)`, exposed in the example through a PvP coinflip action selector
509
+ - unresolved PvP lobby browsing through `client.suigar.getPvPCoinflipGames(...)`, including public join cards while disconnected, an optional private-lobby join toggle, and connected-wallet filtering for cancel
510
+ - wallet connection and execution with `@mysten/dapp-kit-core` and `@mysten/dapp-kit-react`
511
+ - supported coin selection from `client.suigar.getConfig()`
512
+ - connected-wallet balance display for each supported coin in the example app
513
+ - privacy badges and copyable PvP game ids in the lobby UI
514
+ - decoding `BetResultEvent` and PvP events into a persistent event log
515
+ - parsing `BetResultEvent.game_details` with `parseGameDetails`
516
+
517
+ Run it from the repo root with:
518
+
519
+ ```bash
520
+ pnpm install
521
+ pnpm turbo run dev --filter='./apps/playground'
357
522
  ```