@gala-chain/launchpad-sdk 9.0.0-rc.0 → 9.0.0-rc.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.
Files changed (171) hide show
  1. package/CHANGELOG.md +485 -10
  2. package/MIGRATING-TO-9.0.md +43 -1
  3. package/README.md +1 -1
  4. package/dist/LaunchpadSDK.d.ts +412 -217
  5. package/dist/LaunchpadSDK.d.ts.map +1 -1
  6. package/dist/api/LaunchpadAPI.d.ts +2 -140
  7. package/dist/api/LaunchpadAPI.d.ts.map +1 -1
  8. package/dist/auth/SessionAuthService.d.ts +32 -2
  9. package/dist/auth/SessionAuthService.d.ts.map +1 -1
  10. package/dist/auth/session.d.ts +59 -0
  11. package/dist/auth/session.d.ts.map +1 -0
  12. package/dist/auth/types.d.ts +6 -1
  13. package/dist/auth/types.d.ts.map +1 -1
  14. package/dist/constants/endpoints.d.ts +19 -1
  15. package/dist/constants/endpoints.d.ts.map +1 -1
  16. package/dist/constants/enums.d.ts +19 -0
  17. package/dist/constants/enums.d.ts.map +1 -1
  18. package/dist/constants/version.generated.d.ts +1 -1
  19. package/dist/errors/LaunchpadSDKError.d.ts +18 -0
  20. package/dist/errors/LaunchpadSDKError.d.ts.map +1 -1
  21. package/dist/index.browser.d.ts +36 -27
  22. package/dist/index.browser.d.ts.map +1 -1
  23. package/dist/index.browser.esm.js +1 -1
  24. package/dist/index.cjs +1 -1
  25. package/dist/index.esm.js +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/launchpad/bundle-submission.d.ts +57 -2
  28. package/dist/launchpad/bundle-submission.d.ts.map +1 -1
  29. package/dist/launchpad/create-sale-validation.d.ts +19 -0
  30. package/dist/launchpad/create-sale-validation.d.ts.map +1 -0
  31. package/dist/launchpad/transport.d.ts +31 -1
  32. package/dist/launchpad/transport.d.ts.map +1 -1
  33. package/dist/launchpad/writes.d.ts +37 -11
  34. package/dist/launchpad/writes.d.ts.map +1 -1
  35. package/dist/native.cjs +1 -1
  36. package/dist/native.d.ts +1 -1
  37. package/dist/native.d.ts.map +1 -1
  38. package/dist/native.esm.js +1 -1
  39. package/dist/schemas/launchpad.d.ts +3 -2
  40. package/dist/schemas/launchpad.d.ts.map +1 -1
  41. package/dist/schemas/primitives.d.ts +12 -2
  42. package/dist/schemas/primitives.d.ts.map +1 -1
  43. package/dist/schemas/user.d.ts +3 -3
  44. package/dist/schemas/validators.d.ts +3 -3
  45. package/dist/services/ApiKeyService.d.ts +1 -1
  46. package/dist/services/BanService.d.ts +1 -1
  47. package/dist/services/ChartService.d.ts.map +1 -1
  48. package/dist/services/ContentReactionService.d.ts +6 -18
  49. package/dist/services/ContentReactionService.d.ts.map +1 -1
  50. package/dist/services/EventsQueryService.d.ts +47 -0
  51. package/dist/services/EventsQueryService.d.ts.map +1 -0
  52. package/dist/services/GalaChainLockService.d.ts.map +1 -1
  53. package/dist/services/GalaChainTransferService.d.ts.map +1 -1
  54. package/dist/services/HealthService.d.ts +33 -0
  55. package/dist/services/HealthService.d.ts.map +1 -0
  56. package/dist/services/ImageService.d.ts +3 -3
  57. package/dist/services/ImageService.d.ts.map +1 -1
  58. package/dist/services/LaunchpadPriceService.d.ts.map +1 -1
  59. package/dist/services/LaunchpadService.d.ts +34 -42
  60. package/dist/services/LaunchpadService.d.ts.map +1 -1
  61. package/dist/services/MessagesService.d.ts +12 -44
  62. package/dist/services/MessagesService.d.ts.map +1 -1
  63. package/dist/services/ModeratorService.d.ts +2 -2
  64. package/dist/services/ModeratorService.d.ts.map +1 -1
  65. package/dist/services/OverseerService.d.ts +3 -3
  66. package/dist/services/OverseerService.d.ts.map +1 -1
  67. package/dist/services/PlatformStatsService.d.ts +1 -2
  68. package/dist/services/PlatformStatsService.d.ts.map +1 -1
  69. package/dist/services/PoolService.d.ts +28 -65
  70. package/dist/services/PoolService.d.ts.map +1 -1
  71. package/dist/services/RestrictedNamesService.d.ts +1 -1
  72. package/dist/services/RestrictedNamesService.d.ts.map +1 -1
  73. package/dist/services/StreamChatService.d.ts +8 -19
  74. package/dist/services/StreamChatService.d.ts.map +1 -1
  75. package/dist/services/StreamTokenServiceBase.d.ts +2 -1
  76. package/dist/services/StreamTokenServiceBase.d.ts.map +1 -1
  77. package/dist/services/StreamWebSocketService.d.ts +423 -451
  78. package/dist/services/StreamWebSocketService.d.ts.map +1 -1
  79. package/dist/services/StreamingService.d.ts +14 -15
  80. package/dist/services/StreamingService.d.ts.map +1 -1
  81. package/dist/services/TokenBanService.d.ts +1 -1
  82. package/dist/services/TradeService.d.ts +13 -9
  83. package/dist/services/TradeService.d.ts.map +1 -1
  84. package/dist/services/TradingQuotesService.d.ts.map +1 -1
  85. package/dist/services/UserService.d.ts +64 -20
  86. package/dist/services/UserService.d.ts.map +1 -1
  87. package/dist/services/realtime/event-catalog.d.ts +456 -0
  88. package/dist/services/realtime/event-catalog.d.ts.map +1 -0
  89. package/dist/services/shared/pagination-helpers.d.ts +22 -5
  90. package/dist/services/shared/pagination-helpers.d.ts.map +1 -1
  91. package/dist/services/shared/service-validators.d.ts.map +1 -1
  92. package/dist/signing/signers.d.ts +57 -1
  93. package/dist/signing/signers.d.ts.map +1 -1
  94. package/dist/types/api-key.dto.d.ts +1 -1
  95. package/dist/types/ban.dto.d.ts +21 -16
  96. package/dist/types/ban.dto.d.ts.map +1 -1
  97. package/dist/types/chat-messages.dto.d.ts +10 -72
  98. package/dist/types/chat-messages.dto.d.ts.map +1 -1
  99. package/dist/types/comments.dto.d.ts +8 -78
  100. package/dist/types/comments.dto.d.ts.map +1 -1
  101. package/dist/types/common.d.ts +40 -62
  102. package/dist/types/common.d.ts.map +1 -1
  103. package/dist/types/constraints.d.ts +47 -23
  104. package/dist/types/constraints.d.ts.map +1 -1
  105. package/dist/types/content-flag.dto.d.ts +5 -1
  106. package/dist/types/content-flag.dto.d.ts.map +1 -1
  107. package/dist/types/content-reactions.dto.d.ts +0 -4
  108. package/dist/types/content-reactions.dto.d.ts.map +1 -1
  109. package/dist/types/engagement-stats.dto.d.ts +16 -27
  110. package/dist/types/engagement-stats.dto.d.ts.map +1 -1
  111. package/dist/types/events.dto.d.ts +89 -0
  112. package/dist/types/events.dto.d.ts.map +1 -1
  113. package/dist/types/global-feed.dto.d.ts +23 -164
  114. package/dist/types/global-feed.dto.d.ts.map +1 -1
  115. package/dist/types/health.dto.d.ts +32 -0
  116. package/dist/types/health.dto.d.ts.map +1 -0
  117. package/dist/types/launchpad.dto.d.ts +77 -66
  118. package/dist/types/launchpad.dto.d.ts.map +1 -1
  119. package/dist/types/messages.dto.d.ts +168 -118
  120. package/dist/types/messages.dto.d.ts.map +1 -1
  121. package/dist/types/moderator.dto.d.ts +1 -1
  122. package/dist/types/options.dto.d.ts +0 -2
  123. package/dist/types/options.dto.d.ts.map +1 -1
  124. package/dist/types/overseer.dto.d.ts +2 -1
  125. package/dist/types/overseer.dto.d.ts.map +1 -1
  126. package/dist/types/platform-stats.dto.d.ts +3 -3
  127. package/dist/types/realtime-events.dto.d.ts +340 -0
  128. package/dist/types/realtime-events.dto.d.ts.map +1 -0
  129. package/dist/types/restricted-names.dto.d.ts +2 -2
  130. package/dist/types/restricted-names.dto.d.ts.map +1 -1
  131. package/dist/types/stream-chat.dto.d.ts +15 -14
  132. package/dist/types/stream-chat.dto.d.ts.map +1 -1
  133. package/dist/types/streaming.dto.d.ts +73 -104
  134. package/dist/types/streaming.dto.d.ts.map +1 -1
  135. package/dist/types/token-ban.dto.d.ts.map +1 -1
  136. package/dist/types/token-config.dto.d.ts +34 -19
  137. package/dist/types/token-config.dto.d.ts.map +1 -1
  138. package/dist/types/trades-query.dto.d.ts +8 -24
  139. package/dist/types/trades-query.dto.d.ts.map +1 -1
  140. package/dist/types/user.dto.d.ts +126 -50
  141. package/dist/types/user.dto.d.ts.map +1 -1
  142. package/dist/types/websocket-events.dto.d.ts +5 -5
  143. package/dist/types/websocket-events.dto.d.ts.map +1 -1
  144. package/dist/utils/address-formatter.d.ts +15 -0
  145. package/dist/utils/address-formatter.d.ts.map +1 -1
  146. package/dist/utils/auto-pagination.d.ts +1 -1
  147. package/dist/utils/auto-pagination.d.ts.map +1 -1
  148. package/dist/utils/error-factories.d.ts +4 -3
  149. package/dist/utils/error-factories.d.ts.map +1 -1
  150. package/dist/utils/errors.d.ts +53 -10
  151. package/dist/utils/errors.d.ts.map +1 -1
  152. package/dist/utils/http.d.ts +24 -3
  153. package/dist/utils/http.d.ts.map +1 -1
  154. package/dist/utils/message-validators.d.ts.map +1 -1
  155. package/dist/utils/pagination-helpers.d.ts +17 -0
  156. package/dist/utils/pagination-helpers.d.ts.map +1 -1
  157. package/dist/utils/pagination-validation.d.ts +2 -2
  158. package/dist/utils/response-handlers.d.ts.map +1 -1
  159. package/dist/utils/service-validators.d.ts +6 -6
  160. package/dist/utils/service-validators.d.ts.map +1 -1
  161. package/dist/utils/validation-helpers.d.ts +18 -59
  162. package/dist/utils/validation-helpers.d.ts.map +1 -1
  163. package/dist/utils/validation.d.ts +4 -1
  164. package/dist/utils/validation.d.ts.map +1 -1
  165. package/package.json +3 -3
  166. package/dist/services/StreamingEventService.d.ts +0 -441
  167. package/dist/services/StreamingEventService.d.ts.map +0 -1
  168. package/dist/types/comment.dto.d.ts +0 -176
  169. package/dist/types/comment.dto.d.ts.map +0 -1
  170. package/dist/types/streaming-events.dto.d.ts +0 -605
  171. package/dist/types/streaming-events.dto.d.ts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## 9.0.0-rc.0
3
+ ## 9.0.0-rc.1
4
+
5
+ Since 9.0.0-rc.0 (published): the sections "App-owned session" through "The stream socket" below
6
+ (the app-session, browser-wallet, bundle-confirmation, error-answer, path-parameter, `PageInfo`,
7
+ `fetchPools`, admin/overseer, trades/tokens/users/streaming and stream-socket contracts) and the
8
+ later entries of "BREAKING CHANGES" and "Fixes" are new in rc.1; everything else in this section is
9
+ rc.0's. Core peer floor is `>=1.0.0-rc.6 <2`.
4
10
 
5
11
  launchpad-sdk is launchpad-only. Everything that speaks to the GalaChain gateway generically is
6
12
  `@gala-chain/gala-core-sdk`'s (a peer dependency: install it beside launchpad-sdk); everything that
@@ -51,15 +57,23 @@ built declarations of both versions (`pnpm docs:migration`).
51
57
  schemas are zod 4, and it is no longer also a dependency) and the shared libraries. No longer
52
58
  dependencies: `@solana/web3.js`, `@solana/spl-token`, `dotenv`, `@modelcontextprotocol/sdk` (and,
53
59
  since the bridge move, `bs58` and `json-stringify-deterministic`).
54
- - **The bridge loads Solana only when used.** The bridge, balance-on-chain, token-list and wrap methods
55
- load `@gala-chain/gala-core-sdk/bridge` (the one core entry that loads `ethers`,
56
- `@solana/web3.js` and `@solana/spl-token`, core's optional peers) with `import()` at their first
57
- call: an app that never bridges never loads Solana and need not install it; an app that bridges
58
- installs those peers (a missing one is a `ConfigurationError` naming them). The CommonJS bundles
59
- `require` it and the UMD bundle requires it or takes the `GalaCoreSDKBridge` global, so there is
60
- one core copy in every build. A bridge or wrap call made with one wallet is refused
61
- (`ConfigurationError`, nothing sent) if `setWallet` / `setWalletProvider` runs while `/bridge`
62
- loads, rather than running as the next wallet. `getSupportedBridgeTokens()` is `async`.
60
+ - **Bridging is explicit: the `bridge` loader.** The bridge, balance-on-chain, token-list and wrap
61
+ methods run on `@gala-chain/gala-core-sdk/bridge` (the one core entry that loads `ethers`,
62
+ `@solana/web3.js` and `@solana/spl-token`, core's optional peers), which the app hands over:
63
+ `new LaunchpadSDK({ ..., bridge: () => import('@gala-chain/gala-core-sdk/bridge') })` (type
64
+ `CoreBridgeLoader`; a page without a bundler passes a function resolving to the module it loaded
65
+ itself). The UMD bundle no longer looks for a `GalaCoreSDKBridge` global. launchpad-sdk
66
+ names neither that module nor Solana in any built bundle: a bundler (Vite, webpack, Metro)
67
+ resolves every load it sees, a lazy `import()` included, so the earlier `import()` inside the SDK
68
+ made every app's build need Solana (an app's Vite build failed with 13 `MISSING_EXPORT` errors on
69
+ `@solana/web3.js` / `@solana/spl-token`). Without the loader a bridge or wrap call is a
70
+ `ConfigurationError` (`field: 'bridge'`) naming it; with it, the module is loaded once per SDK at
71
+ the first such call, and a failed load (the peers are not installed) is a `ConfigurationError`
72
+ naming them, tried again at the next call. `check:pack` scans every built bundle of the packed
73
+ package for a `@gala-chain/gala-core-sdk/bridge` or `@solana/*` load (`scripts/runtime-specifiers.ts`).
74
+ A bridge or wrap call made with one wallet is refused (`ConfigurationError`, nothing sent) if
75
+ `setWallet` / `setWalletProvider` runs while `/bridge` loads, rather than running as the next
76
+ wallet. `getSupportedBridgeTokens()` is `async`.
63
77
  - **The public surface re-exports nothing** (`tests/public-surface.test.ts` pins both entries' export
64
78
  lists and fails on any export declared outside `src/`). Removed from the main and `/native`
65
79
  entries: `generateEIP712Types` and `GalaChainSigner` (core's), the duplicate EIP-712 types
@@ -68,8 +82,469 @@ built declarations of both versions (`pnpm docs:migration`).
68
82
  (core's), `generateUniqueKey` (core's `createUniqueKey`), and the demo `.env` loaders `getEnv`,
69
83
  `getEnvOrThrow`, `loadEnvWithFallback`.
70
84
 
85
+ ### App-owned session (a browser app's httpOnly-cookie JWT session)
86
+
87
+ - **`session` in `LaunchpadSDKConfig`.** A browser app (launchpad-frontend-v3) keeps its own JWT
88
+ session: the refresh token is an httpOnly cookie the backend sets for `cookieSession: true`
89
+ logins, and the app is the one token authority. With `session: { accessToken, onUnauthorized,
90
+ credentials? }` every launchpad-backend request takes its bearer from `accessToken()` (awaited per
91
+ request), a 401 goes to `onUnauthorized(rejectedToken)` and is retried once with the token it
92
+ returns (`null`: the 401 stands), the launchpad-backend client sends credentials (unless
93
+ `credentials: 'omit'`), and the SDK stores no token (services that attach "the JWT" attach
94
+ nothing; the client adds the session's bearer). No other host (gateway, bundler, dex-api, swap
95
+ backend) ever sees the session or credentials. Exclusive with `accessToken`. Pair it with
96
+ `signer` + `walletAddress`: `wallet` / `walletProvider` sign every launchpad request (`Sign`).
97
+ - **`loginWithSignature({ address, message, signature, publicKey? })`**: `POST /auth/login` with a
98
+ signature the caller made (a browser wallet signs `Sign in to Launchpad t:<ms>` itself); with a
99
+ session it asks for the cookie session and stores nothing. **`loginWithDevBypass(address)`**: the
100
+ backend's non-production signature bypass.
101
+ - With a session, `refreshToken()` is `POST /auth/refresh` with `{}`, no bearer and the cookie;
102
+ `logout({ allDevices, accessToken })` always revokes on the server (bearer as handed over, never
103
+ refreshed); `ensureValidToken()` is the session's current token. Auth-endpoint failures are an
104
+ `AuthError` with the backend's `statusCode` (401/403: the session was rejected; 429/5xx/none:
105
+ transient). `tests/app-session.test.ts` drives each through a real axios instance.
106
+
107
+ ### Browser-app wallet signing
108
+
109
+ - **`BrowserWalletSigner.fromEip1193(provider, keyAddress, options?)`**: a browser app holds the
110
+ wallet's raw EIP-1193 provider, not an ethers one. The factory builds the ethers `BrowserProvider`
111
+ the typed-data path reads the network and the selected account from, keeps the raw `request` for
112
+ `signObject`, and takes the key as `0x` or `eth|` (a `client|` alias is refused: it names no key).
113
+ The bytes are those of the constructor path (bundler-typed-data and wallet-providers goldens
114
+ unchanged; `tests/wallet-signers-app.test.ts` compares both paths over a real `BrowserProvider`).
115
+ - **`WALLET_ACCOUNT_CHANGED`** (`LaunchpadSDKError`, `details.walletAccount` / `signerAccount`):
116
+ `BrowserWalletSigner` refuses to sign when the wallet's selected account is not its key. Same
117
+ message as before; it was a plain `Error`, which the bundler transport rewrapped as
118
+ `LAUNCHPAD_SIGNING_FAILED`.
119
+ - **`WALLET_USER_REJECTED`**: the bundler transport tells a decline in the wallet (gala-core-sdk's
120
+ `isWalletUserRejection`: `4001`, `ACTION_REJECTED`, Gala Wallet's decline, core's
121
+ `SIGNING_FAILED`/`user_rejected`) apart from other signing failures, with the wallet's code in
122
+ `details.walletCode`. Other failures stay `LAUNCHPAD_SIGNING_FAILED`; an SDK error passes through.
123
+ - **Gala Wallet typed data selects the account.** `GalaWalletSigner.signTypedData` switches the
124
+ extension (`setAddress`, where it offers it) to `selectAddress`, else the Ethereum account the
125
+ request names, inside gala-core-sdk's per-wallet select-then-sign queue
126
+ (`withGalaWalletIdentity`), and signs through the `wallet` option when one is given (it read
127
+ `window.gala` even then, while `signObject` used the option).
128
+ - **`GalaWalletSigner` option `verifyActiveAccount` (default `false`)**: the app's check before a Gala
129
+ Wallet signature. On, the signer reads the wallet's active account (`wallet.address`, else
130
+ `eth_accounts` when the wallet has not reported one since the page loaded) and refuses with
131
+ `WALLET_ACCOUNT_CHANGED` (`details.walletAccount` null when none was reported) unless it is the
132
+ signer's key; the RPC order is that check, `setAddress`, `eth_signTypedData`. Off by default so the
133
+ six recorded `galaWalletEthAddress` vectors of `bundler-typed-data.golden.json` are unchanged (the
134
+ check adds an `eth_accounts` request); the opt-in sequences are pinned by the new
135
+ `tests/golden/gala-wallet-verify-account.golden.json`. A signer that names no Ethereum key
136
+ (`client|` alias without an Ethereum `selectAddress`) is refused at construction
137
+ (`INVALID_SIGNER_OPTIONS`). `BrowserWalletSigner` needs no option: it always compares the wallet's
138
+ selected account with its key.
139
+
140
+ ### Bundle confirmation for a browser app
141
+
142
+ - **`bundleSocketFactory` in `LaunchpadSDKConfig`** (passed to `BundlerTransport`): the app's own
143
+ bundler socket, with its reconnect settings. Each `wait()` adds its listener for the bundle's
144
+ event on the socket the factory returns and removes it when the wait ends; that socket is never
145
+ disconnected (a socket the transport opens itself still is).
146
+ - **`sdk.launchpad.attach({ bundleId, method, uniqueKey, tokenName? })`** (and
147
+ `BundlerTransport.attach`): a `BundleSubmission` for a bundle sent before a reload, whose `wait()`
148
+ confirms exactly as the original's (socket event raced against the REST confirmation, which also
149
+ answers for a bundle that settled while nobody listened). Nothing is signed or sent until
150
+ `wait()`. Refused with `INVALID_BUNDLE_ATTACHMENT` (`details.field`): empty `bundleId` /
151
+ `uniqueKey`, a method the bundler does not accept, a `CreateSale` without `tokenName`. New exports:
152
+ `LAUNCHPAD_WRITE_METHODS` (`LaunchpadWriteMethod` is now derived from it), types
153
+ `BundleAttachment`, `BundleSocket`, `SocketFactory`. `LaunchpadWriteTransport` gains `attach`.
154
+ - The bundler (bundler-backend origin/main, `src/socket/socket.gateway.ts`) emits only
155
+ `{ status, data }` (lowercase; `PENDING` / `PROCESSED` / `FAILED`), never `{ Status, Data }`, so
156
+ `parseBundleResult` is unchanged.
157
+
158
+ ### The backend's error answer on `NetworkError`
159
+
160
+ - **`serverMessage`, `serverError`, `details`, `body`** (type `NetworkErrorServerAnswer`) beside
161
+ `statusCode`. Before, an HTTP failure was `NetworkError("HTTP <M> <url>: Request failed with status
162
+ code N")` and the backend's body survived only on `originalError.launchpadError`. The launchpad
163
+ backend (origin/main `GlobalExceptionFilter`) answers `{ status, error: true, message }`
164
+ (HttpException: validation pipe, `sendResult`, guards) or `{ statusCode, message }` (CustomError,
165
+ 413, unhandled 500): `serverMessage` is that `message` (an array is joined with `,`, as
166
+ `String(array)` shows it), `serverError` is a string `error` only (`true` is not one), `body` the
167
+ whole body (or the text of a non-JSON answer). Messages are unchanged.
168
+ - **A wrapped NetworkError keeps them**: `new NetworkError(msg, status?, originalError?, answer?)`
169
+ takes the answer of `originalError` (an axios error's response, or another NetworkError) when
170
+ not given, and `handleNetworkError` (the `error-wrapper` `withErrorHandling` wrap services put
171
+ around the client's error) also takes the wrapped NetworkError's status: it was `undefined`
172
+ there. (A bare `createNetworkError(msg, undefined, inner)` does not take the status: the
173
+ gateway-reads golden pins `statusCode` absent on the GalaChain balance read's wrapped 400.)
174
+ `createNetworkError` takes the answer as an optional fourth argument;
175
+ `response-handlers.assertResponseSuccess` puts a 2xx error envelope's body on it.
176
+ - Paths that turned the client's NetworkError into another error now keep it a NetworkError, same
177
+ message: `PoolService.fetchToken`'s 404 "Token not found: <name>" (was a plain `Error`),
178
+ `UserService.uploadProfileImage`'s "Profile image upload failed: ..." for an HTTP refusal (was a
179
+ `ValidationError` `UPLOAD_FAILED`; other failures still are), and `LaunchpadPriceService`'s spot
180
+ price failure now wraps the original.
181
+ - The response interceptor's `launchpadError.error` is set only when the body's `error` is a string
182
+ (it cast the backend's `true` to a string).
183
+
184
+ ### Path parameters are spelled as the backend compares them
185
+
186
+ - `buildEndpoint` (every `StreamTokenServiceBase` service) lowercased EVERY path value. It now
187
+ follows `PATH_PARAM_CASING`, decided per parameter from launchpad-backend origin/main's routes:
188
+ `tokenName` (the pool lookup's `normalizeTokenName`), `address` and `userAddress`
189
+ (`normalizeAddress`) stay lowercased; `assetId` and `targetId` (Mux ids, mixed-case, compared
190
+ exactly), `id` (numeric invite ids) and `code` (invite codes, matched exactly) are sent as given.
191
+ A parameter the table does not name is refused. Changed on the wire: `getRecordingDownload` /
192
+ `deleteRecording` (`/v1/tokens/:tokenName/recordings/:assetId[/download]`) and
193
+ `removeSimulcastTarget` (`/v1/tokens/:tokenName/simulcasts/:targetId`): a mixed-case Mux id was
194
+ lowercased into one the backend does not have; the moderator and overseer `getInviteByCode`
195
+ (`/v1/{moderator,overseer}-invites/code/:code`) send the code as given (generated codes are
196
+ lowercase hex, so only a code typed in another case differs); the invite `:id` routes are
197
+ unchanged in effect (digits).
198
+
199
+ ### `PageInfo` is the backend's `pageInfo`
200
+
201
+ - **`PageInfo` = `{ hasNextPage, endCursor?, hasPreviousPage?, startCursor?, totalCount? }`**, the
202
+ backend's own keys (launchpad-backend origin/main `buildPageInfo`: `endCursor` / `startCursor`
203
+ are `string | null`). It declared `nextCursor` / `prevCursor` / `hasPrevPage`, while every list
204
+ but two passed the backend's `pageInfo` through unchanged (so at run time it was `endCursor`), and
205
+ `PoolService` / `UserService` renamed the backend's keys to the declared ones. Now no list renames:
206
+ pools and user token lists return the backend's `pageInfo` (`pageInfoOf`, which drops a field of
207
+ the wrong type), and the cursor walkers (`autoPaginateWithCursor`, `fetchAllPools`, the vault
208
+ token-name lookup) follow `endCursor` (`nextCursorOf`): `autoPaginateWithCursor` stopped after
209
+ the first page of every passed-through list. They also stop when the backend repeats the cursor
210
+ just used (`fetchAllPools` went back to page 1 on a missing cursor).
211
+ - `totalCount` is set only on results the SDK assembles from every page (`fetchAll...`): the
212
+ backend sends no count. Removed with it: `UserTokenListResult.total` (always undefined), the
213
+ dead `InternalFetchUserBalancesResponse` type, and the top-level `count` / `total` / `nextCursor`
214
+ / `prevCursor` / `hasNextPage` / `hasPrevPage` fields of the internal pool and user-list response
215
+ types (the backend answers `{ tokens | token | launches, pageInfo }`). Migration: read
216
+ `pageInfo.endCursor` (`?? undefined` for a `cursor` option) where you read `nextCursor`.
217
+
218
+ ### `fetchPools` matches `GET /v1/tokens` (launchpad-backend origin/main `FetchPoolDto`)
219
+
220
+ - **`type` and `streamStatus` in the backend's spelling, any case.** `type` RECENT / POPULAR /
221
+ RECORDINGS / SECONDS_STREAMED (`POOL_FETCH_TYPES`; the backend compares case-sensitively, so it is
222
+ sent uppercase), `streamStatus` IDLE / ACTIVE / DISABLED (`POOL_STREAM_STATUSES`). The lookup
223
+ tables were lowercase-keyed and DROPPED the app's `'POPULAR'` / `'ACTIVE'` without a word; lowercase
224
+ still works. A value the backend does not accept is a `ValidationError`, never dropped.
225
+ - **`pageSize` 1 to 100** (`POOL_LIST_MAX_PAGE_SIZE`, the backend's `MediumListQueryDto` maximum):
226
+ it was silently clamped to 20. Larger, zero or fractional is a `ValidationError`. `fetchAllPools`
227
+ reads pages of 100 (was 20). Default still 10.
228
+ - `recentlyStreamed` (integer 1 to 720), `language` (ISO 639-1) and `search` (at most 255) are
229
+ checked before sending, as the backend would refuse them.
230
+ - **`hasUpcomingShows` / `hasRecordings`: only `true` is sent.** The backend's boolean conversion
231
+ (`@Type(() => Boolean)` with implicit conversion) reads `"false"` as true, so `false` used to turn
232
+ the filter ON; it now sends nothing (no filter).
233
+ - `isGraduated` / `creatorAddress` are NOT added: origin/main's `FetchPoolDto` has neither and the
234
+ pipe forbids unknown properties (a 400).
235
+ - One pool-list reader: the private `LaunchpadAPI.fetchPools` / `fetchPoolsFromAPI` copy (used to
236
+ find a vault's token name for a sell-fee quote) is removed; that lookup reads `PoolService`. The
237
+ copy read `hasNextPage` / `nextCursor` at the top of `data`, where the backend never puts them,
238
+ so it never got past page 1. `LaunchpadService` / `fetchAllPools` take `FetchPoolsOptions` (their
239
+ inline option types still named `offset` / `limit`). The zod `poolFetchTypeSchema` takes all four
240
+ types; the public `PoolFetchType` is the one type (`'RECENT' | 'POPULAR' | 'RECORDINGS' |
241
+ 'SECONDS_STREAMED'`, it was the schema's two).
242
+
243
+ ### Admin and overseer contracts (launchpad-backend origin/main)
244
+
245
+ - **`revokeOverseerInvite` and `revokeOverseer` sent no Authorization.** The auth headers were passed
246
+ in the query-params slot of `HttpClient.delete`: the request went out unauthenticated and the
247
+ header names became query parameters. They are headers now (`tests/g3-admin-wire.test.ts`).
248
+ - **Restricted token names carry the session bearer.** `getRestrictedNames` / `updateRestrictedNames`
249
+ (Overseer-only routes) sent no Authorization. An empty `content` is accepted (the backend takes any
250
+ string; it clears the list); `UpdateRestrictedNamesResponse` is `{ content }` (the backend echoes the
251
+ stored content), it was typed `{ success: true }`.
252
+ - **Invite codes are validated like the backend**: any non-empty string up to 64 characters
253
+ (`INVITE_CODE_CONSTRAINT.MAX_LENGTH`, was 100 for overseer codes and "64 hex" for moderator codes).
254
+ An unknown code is the backend's 404, which a claim page relies on to fall through from the overseer
255
+ lookup to the moderator one; it used to be a client-side `ValidationError`.
256
+ - **`sdk.getHealth()`** (`GET /health`, public): `HealthCheckResponse` (`status` plus database, redis,
257
+ mux, rabbitmq, galachain_query, galachain_launchpad). A critical dependency down is HTTP 503 and
258
+ throws a `NetworkError`.
259
+ - **`sdk.getEventStats()`, `sdk.queryEvents(options)`** (`GET /v1/events/stats`, `GET /v1/events`,
260
+ Admin API key or Overseer): the routes answer BARE bodies, which the service reads and shape-checks.
261
+ `queryEvents` pages by `page` / `limit` (1..100). `eventTypes` / `categories` are sent as a
262
+ REPEATED key (one value is sent twice): the backend validates them with `@IsArray` behind Express 5's
263
+ simple query parser, where a comma list or `key[]=` is refused.
264
+ - **`getMessageStats` reads the bare object** `GET /v1/messages/stats` answers (it required a
265
+ `{ data }` envelope and could never succeed against the backend).
266
+
267
+ - **The flag filters validate what `GetFlagsQueryDto` takes**: the token filter is 1-50 letters and digits
268
+ (a 1-character filter threw; see the token name rules below) and the addresses
269
+ (`reporterAddress`, `reportedUserAddress`, also `createFlag`'s) are the forms `@GalaChainAddressField()`
270
+ takes after its normalisation: `eth|<40 hex>`, `0x<40 hex>`, 40 hex, `client|<24 hex>` or 24 hex
271
+ (`isBackendUserAddress` / `assertBackendUserAddress`). A bare client id is accepted now; a partial
272
+ address and `client|<name>` stay refused, because the backend answers 400 for them.
273
+ - **`updateModeratorInvite({ inviteId, description: '' })` is a valid update**: the backend's `UpdateModeratorInviteDto.description` has no minimum and its service updates on
274
+ `description !== undefined`, so `''` alone clears the label (the SDK refused it with "At least one of
275
+ role or description"). An update with neither field is still refused.
276
+
277
+ ### Trades, tokens, users and streaming contracts (launchpad-backend origin/main)
278
+
279
+ - **`fetchTrades` reaches its route.** It sent the literal `/v1/tokens/:tokenName/trades` with the
280
+ name in a query parameter; the token is now in the path (lowercased, as the backend compares it).
281
+ `TradeQueryItem` types `tokenSymbol`, which `GET /v1/trades` returns. `getTrades` already unwrapped
282
+ the `{ data }` body (verified on the wire, `tests/g3-trades-wire.test.ts`).
283
+ - **`fetchTokenStats` returns what the backend sends**: `{ holdersCount, chatMessageCount, commentCount,
284
+ roomCount }` (`TokenStatsData`, was `EngagementStatsData`). It required a `stats` wrapper the backend
285
+ has none of, and so could never succeed; `totalEngagement`, participants and activity fields were
286
+ never sent.
287
+ - **`updateTokenConfig` takes `chatEnabled`** (was `messagesEnabled`, which the backend refuses with 400:
288
+ its `UpdateTokenConfigDto` has `chatEnabled`).
289
+ - **`fetchLaunchpadFee()` returns the number** (`data.feeAmount`); it returned the `{ feeAmount }` object
290
+ typed as a number.
291
+ - **Images reach their routes.** `uploadImageByTokenName` posts to `/v1/tokens/:tokenName/image` (the
292
+ placeholder went out literally, the name in a query parameter) and `uploadProfileImage` to
293
+ `POST /v1/users/me/image` (it used the token route); the profile upload needs no address or wallet.
294
+ - **`updateProfile` sends what is given.** `address` is optional (validated, not sent), `fullName` and
295
+ `profileImage` are optional (at least one), `profileImage: ''` clears the image, and the profile is no
296
+ longer fetched first to send the old image back. `fullName` is validated like the backend's
297
+ `UpdateProfileDto` (a trimmed 2..20 letters, one optional second word) and no wallet is required, only
298
+ a JWT.
299
+ - **User token lists take pages up to 100** (`USER_PAGINATION_CONSTRAINT.MAX_LIMIT` 20 -> 100; the
300
+ backend takes up to 1000, default 50); `getManagedTokens` sends no `pageSize` unless asked, so the
301
+ backend's 50 applies (the SDK's default 10 cut a longer list off).
302
+ - **`fetchVolumeData` returns the chart**: `{ dataPoints, isPreviousData, nextTime? }` with the candles
303
+ typed as the backend sends them (`startTime`, `openPrice`, `highPrice`, `lowPrice`, `closePrice`,
304
+ `volume`); `dataPoints` held the whole `{ data, isPreviousData }` object typed as `{ timestamp, value }[]`.
305
+ - **`getTradeQuote` takes an `AbortSignal`** (`TradeQuoteParams.signal`); `PlatformStatsData` volumes are
306
+ numbers (typed as strings); `listOverseerUsers` accepts `ASC` / `DESC` (the backend reads the order
307
+ case-insensitively).
308
+ - **Recordings match `GET /v1/tokens/:tokenName/recordings`**: a bare array with no paging.
309
+ `getStreamRecordings({ tokenName })` returns `{ recordings, tokenName }` (the fabricated `page`, `limit`,
310
+ `total`, `hasNext` are gone; the old code read `data.recordings` and could never succeed). A recording is
311
+ `{ id, playbackId, status, duration, createdAt, masterAccess, aspectRatio?, assets? }` (was `assetId`,
312
+ `tokenName`, `downloadable`, `fileSize`, `thumbnailUrl`); `RECORDING_STATUS` is Mux's `preparing` /
313
+ `ready` / `errored`. An unknown token is a `NetworkError` with `statusCode` 404.
314
+ - **Simulcasts match `/v1/tokens/:tokenName/simulcasts`**: the list is a bare array behind the owner's
315
+ session bearer (it was sent without Authorization); a target is `{ id, url, status, passthrough? }`;
316
+ `addSimulcastTarget` sends `{ platform, url, streamKey, passthrough? }` with the platform in lowercase
317
+ (`'youtube' | 'twitch' | 'facebook' | 'custom'`, `SIMULCAST_PLATFORM`), as the backend's
318
+ `CreateSimulcastTargetDto` requires (it refused `rtmpUrl`, `name` and uppercase platforms), and the
319
+ URL is RTMP or RTMPS only (the SDK allowed SRT, which the backend refuses).
320
+ - **`sdk.getChatStatus(tokenName)`** (`GET /live/:tokenName/chat`, public): `{ enabled, tokenName,
321
+ reason?, slowModeSeconds?, status? }`. `ChatStatusResponse` gains `tokenName` and `slowModeSeconds`.
322
+ - **The pinned message is the backend's `PinnedMessageData`** (`messageId`, `content`, `userAddress`,
323
+ `pinnedBy`, `pinnedAt`, `user?`, `holderTier?`): `getPinnedMessage` returned a type with `id` and
324
+ `createdAt` the backend never sends, and threw when nothing was pinned (`{ data: null }`); it returns
325
+ `{ message: null }` now. `getPinnedChatMessage` / `pinChatMessage` return that message as it is, and
326
+ `unpinChatMessage` takes its `messageId`.
327
+ - **`UpdateMessageOptions.type`**: a chat edit is capped at 500 characters before it is sent when the
328
+ type is known (`updateChatMessage` passes it).
329
+ - **`PoolData` names the streaming fields the backend sends**: `muxPlaybackId`, `muxStreamAspectRatio`,
330
+ `streamLanguage` (typed `playbackId`, `aspectRatio`, `language`, which the backend does not send, so
331
+ they were always undefined); `streamEnabled`, `messagesEnabled`, `commentsEnabled` are not on a pool.
332
+ - **New `sdk.getUserGalaBalance(address)`** (`GET /v1/users/:address/gala-balance`, public: the GALA
333
+ balance and USD value the backend reads) and **`sdk.fetchReferralCode()`**
334
+ (`GET /v1/connect/users/referrals/code`, JWT: `{ code, username? }`).
335
+ - **`TradesQueryResult` is the one trades result type, and it is not deprecated.** `getTrades`,
336
+ `getRecentTrades` and `getHolderHistory` return the backend's `{ trades, pageInfo }`; the
337
+ `@deprecated` marker (it pointed at `TradesListResult`, an `items`/`meta` type no method returns) and
338
+ `TradesListResult` are gone, and the `getTrades` examples read `pageInfo` (they destructured a `meta`
339
+ the result never had).
340
+ - **`UpdateTokenConfigOptions` is one type.** The package root exported a `token-config.dto` interface
341
+ (social links `string` only) while `updateTokenConfig` took a second one declared beside the message
342
+ types (`string | null`). The backend's `UpdateTokenConfigDto` takes `null` to clear a social link, so
343
+ the one type is the `string | null` form, declared in `token-config.dto` with
344
+ `validateUpdateTokenConfigOptions` (moved with it), and the dead duplicate
345
+ `InternalUpdateTokenConfigResponse` of `messages.dto` is gone.
346
+ - **The pin and invite result types are exported, one meaning per name.** `pinMessage` / `unpinMessage`
347
+ return `PinMessageResult` (`{ success }`) and `getPinnedMessage` returns `PinnedMessageResult`
348
+ (`{ message }`), both now exported from the root; the root's old `PinMessageResult`
349
+ (`{ tokenName, pinnedMessage }`, the shape of a removed REST route) is gone and
350
+ `getPinnedChatMessage` / `pinChatMessage` return `GetPinnedMessageResult`
351
+ (`pinnedMessage` is `null` when another moderator unpinned between the pin and the read back; it was
352
+ cast to non-null). `InviteScope`, `UpdateInviteRoleOptions` and `UpdateInviteRoleResult` are exported;
353
+ the identical `InviteScope` of `streaming.dto` and `ModeratedTokenInviteScope` are the one
354
+ `InviteScope`.
355
+ - **`fetchProfile` is typed and unwrapped.** `fetchProfile(address)` returned the whole
356
+ `{ status, error, message, data }` envelope typed `unknown`; it returns `PublicUserProfile` (the
357
+ backend's `ProfileResponse`: `address`, nullable `fullName` / `profileImage` / `bio`, `stats`
358
+ (`ProfileStats`), the four `BadgeStatus[]` ladders, `isLiveStreaming`, `liveStreamToken`) and a body
359
+ that is not that shape is a `NetworkError` (`Malformed profile response`). The service's no-address
360
+ form (`GET /v1/users/me/profile`, JWT) returns `CurrentUserProfile`. `BadgeStatus`, `ProfileStats`,
361
+ `PublicUserProfile` and `CurrentUserProfile` are exported.
362
+ - **`PoolData` types `badges`** (`PoolBadges`: the `volume` and `engagement` `BadgeStatus[]` ladders the
363
+ backend builds for `GET /v1/tokens` and `/:tokenName`) and the stored `volumeBadge` / `engagementBadge`
364
+ columns. The pool route sends no `chatEnabled`, `streamingEnabled` or `tokenSymbol` (they are not in
365
+ its attribute list), so `PoolData` does not declare them.
366
+ - **Message, comment and chat types are the backend's five shapes** (`GET /v1/messages` and the four write
367
+ answers differ; the SDK typed one invented `BaseContentItem` for all of them). `Message` is the list
368
+ item of `fetchMessages`, `getComments` and `getChatMessages`: `id` and `messageId` (the same string),
369
+ `user` / `userProfile` (the same non-null `{ fullName, profileImage }`), `updatedAt` `string | null`,
370
+ `reactions: { counts, userReacted? }` (was a plain `Record<string, number>`), and no `type`,
371
+ `holderTier` or `isPinned` (the backend sends none of them on a list item). Writes: `sendChatMessage`
372
+ returns `SentChatMessage` (no database id, `holderTier?`), `createComment` / `updateComment` return
373
+ `WrittenComment` (numeric `id` AND `messageId`, no `tokenName`), `updateChatMessage` returns
374
+ `EditedChatMessage` (numeric `id`, `reactions?` as `ReactionData`); `createMessage` / `updateMessage`
375
+ infer the result from `options.type`. `BaseContentItem`, `CommentItem`, `ChatMessageItem`,
376
+ `isChatMessageItem` and the dead `/v1/comments` result types (`CommentData`, `FetchCommentsResult`,
377
+ `PostCommentResult`, `FetchCommentsOptions`, `PostCommentOptions`, `DeleteCommentOptions`, file
378
+ `comment.dto.ts`) are gone; `isMessage` checks the real shape (it needed a `type` no item has, so it
379
+ was false for every item). Each id the examples took from a write (`result.comment.id`, a number, or
380
+ `result.message.id`, undefined) is the `messageId`.
381
+ - **Each method validates the token name of ITS backend route** (launchpad-backend origin/main). The SDK
382
+ refused names the routes take (3-20 everywhere, 2-20 on messages, 50 on trades) and sent names a route
383
+ refuses. Five rules, exported as `TOKEN_NAME_*_CONSTRAINT` (and `TokenNameConstraint`), replace
384
+ `LAUNCHPAD_TOKEN_NAME_CONSTRAINT`, `RELAXED_TOKEN_NAME_CONSTRAINT`, `EXTENDED_TOKEN_NAME_CONSTRAINT`
385
+ and the helpers' silent 3-20 default (`validateTokenName(name, field, constraint)`,
386
+ `isValidTokenName(name, constraint)`, `validateOptionalTokenName(...)` now take the route's rule):
387
+ `DTO` 1-20 letters and digits (`@TokenNameField()` request fields: `fetchPools` filter, `fetchVolumeData`,
388
+ `fetchTokenDistribution`, `fetchUserHolderContext`, `fetchMessages`, `createMessage`, `getTrades`,
389
+ flag create, moderator invites, `banToken`), `FILTER` 1-50 (the flag lists), `PATH` letters and digits
390
+ 1-255 (a raw `:tokenName` the pool lookup resolves, no format check: `fetchToken`, `fetchTokenStats`,
391
+ `updateTokenConfig`, `getTokenConfig`, image upload, every stream, recording, simulcast, ban and chat
392
+ method, `getPinnedMessage`, the user token list filter), `AVAILABILITY` 1-50 characters, any
393
+ (`validateToken`, `checkPool`: `ValidateTokenDto`), `EVENT` 1-100 characters without HTML (the
394
+ `WebSocketAdminService` emits). The root helpers `validateTokenName(name, field, constraint)`,
395
+ `isValidTokenName(name, constraint)` and `validateOptionalTokenName(name, field, constraint)` take the
396
+ rule of the route you name (they had a silent 3-20 default). The chain's launch rule (letters and
397
+ digits, 1+, no cap; what the launch form and `createSale` take) is `tokenNameSchema`,
398
+ `launchTokenDataSchema` and `imageUploadOptionsSchema`; `graphDataOptionsSchema` and
399
+ `graduateTokenOptionsSchema` use the DTO rule (`dtoTokenNameSchema`) and `checkPoolOptionsSchema` the
400
+ availability rule (`availabilityTokenNameSchema`). `TRADES_QUERY_CONSTRAINTS` (a deprecated
401
+ composite) is gone.
402
+ - **The reaction routes take no token name.** `AddReactionDto` is `{ messageId, reactionType }` and the
403
+ backend refuses any other property (whitelist + forbidNonWhitelisted), so `addContentReaction` with a
404
+ `tokenName` answered 400; `AddContentReactionOptions` / `RemoveContentReactionOptions` lose
405
+ `tokenName` and nothing is validated or sent for it.
406
+ - **`uploadTokenImage` and `uploadProfileImage` work on a signer-plus-address SDK.** The facade threw
407
+ "Wallet is required" (and `uploadProfileImage` defaulted the address from the wallet) though
408
+ `POST /v1/tokens/:tokenName/image` and `POST /v1/users/me/image` read the user from the session JWT and
409
+ need nothing else; an app configured with `{ signer, walletAddress, session }` and no `wallet` uploads
410
+ with the session bearer (`tests/g7-uploads-wire.test.ts`). `uploadProfileImage` no longer invents an
411
+ address: one that is given is validated and left out of the request. Their JSDoc described
412
+ `imagePath` options and a Node-only API that do not exist (the options are `{ file }`).
413
+ - **Image uploads send multipart, not JSON.** Both uploads put the `FormData` through an axios instance
414
+ that carries `Content-Type: application/json` as an instance default; the request-level header was only
415
+ left out, so the default survived the merge and axios serialised the body as JSON
416
+ (`{"image":{}}`, the file lost; the 400 the backend answers was the only sign). `HttpClient` now
417
+ clears the Content-Type for a `FormData` body per request (an explicit `undefined`), so the transport
418
+ sets `multipart/form-data` with its boundary (axios' Node adapter, the browser's XHR). The existing
419
+ wire tests swapped the whole instance for one without the default and could not see it;
420
+ `tests/image-upload-multipart-wire.test.ts` (Node, real server) and
421
+ `tests/image-upload-multipart-browser-wire.test.ts` (jsdom XHR, real server, plus the FormData as it
422
+ leaves axios) run the real `createHttpExecutor` instance and read the bytes sent. These are the only
423
+ two multipart paths in the SDK.
424
+ - **One name for an image upload.** `UploadImageByTokenNameOptions.options.tokenName` (a second name that
425
+ went into the URL without being validated) is gone; the path takes the validated `tokenName`. The dead
426
+ `LaunchpadAPI.uploadImageByTokenName` (`POST /launchpad/upload-image`) and
427
+ `LaunchpadAPI.fetchTokenDistribution` (`GET /holders/:tokenName`), routes the backend does not have, are
428
+ removed with their tests (`ImageService` and `PoolService` are the working ones).
429
+
430
+ ### The stream socket: one registry of subscribers (launchpad-backend origin/main)
431
+
432
+ `StreamWebSocketService` keeps ONE registry. `sdk.realtime` is the service of an SDK (built with
433
+ `streamWebSocketUrl`, the reconnect settings and the session's token). Every event the backend emits has a
434
+ typed entry on it (`RealtimeEventPayloads`, read from the emit sites, not from the backend's docs).
435
+
436
+ - **Any number of subscribers per event, per token, per room.** Every `subscribe*` method is synchronous,
437
+ returns that subscriber's OWN unsubscribe (a second call is harmless) and replaces nobody. The old
438
+ single callback slots (`globalCallbacks`, `roomCallbacks`, `globalFeedCallbacks`) are gone: a second
439
+ subscriber used to replace the first and the first's cleanup deleted the second's callback, and
440
+ `subscribeToStream` on a joined token wiped every other subscriber's callbacks. A subscriber that
441
+ throws or rejects is logged and skipped; the others are served. `LaunchpadSDK.watch*` are plain
442
+ subscribers (the fan-out, refcount and room bookkeeping `watchTokenRoomEvent` kept is deleted).
443
+ - **Rooms are refcounted.** A token room, a comment room, the global feed and the events firehose are
444
+ joined on the FIRST subscriber and left on the LAST; never two server subscriptions for one room. A
445
+ leave waits for the join's acknowledgement (a subscribe followed at once by an unsubscribe cannot strand
446
+ the room on the server). A subscribe before the socket is connected registers and joins on connect; with
447
+ `autoConnect` (default) it also opens the socket. It never throws `NOT_CONNECTED`.
448
+ - **Listeners are registered once per socket**, when it is created (they used to be registered again on
449
+ every `connect`, so every reconnect doubled every callback), and removed when it is closed.
450
+ - **Reconnect.** The service acts on the Socket's `connect` (the Manager's `reconnect` fires before the
451
+ socket is connected and used to run `authenticate()` too early). On every connection after the first:
452
+ re-authenticate FIRST with the token current at that moment (`tokenProvider`, never a remembered
453
+ one) if the socket was authenticated, THEN re-join every room that still has a subscriber, THEN call the
454
+ new `onReconnect(listener)` listeners (the app's version never fired, `reconnect` is a Manager event).
455
+ `onConnectionStateChange(listener)` is new. A server-side `io server disconnect` reconnects (socket.io
456
+ does not), and so do `connect()` and a new subscribe after socket.io ran out of reconnection attempts
457
+ (`socket.active` stays true then). `LaunchpadSDKConfig.streamReconnectAttempts` / `streamReconnectDelay` /
458
+ `streamReconnectDelayMax` reach socket.io (a browser app wants `Infinity`, 1000, 10000); defaults stay
459
+ 5 / 2000.
460
+ - **`authenticate(token?)` returns `Promise<boolean>`** (it returned `void` and threw `NOT_CONNECTED`
461
+ when not connected): `true` on `authenticated`; `false` on `exception`, `stream_chat_auth_error`, the
462
+ 10 s timeout, a dropped connection, no token, or a newer `authenticate` / `deauthenticate`. The same
463
+ token on the same connection sends nothing; a different token replaces the pending one and is sent after
464
+ the server answered the one in flight. `LaunchpadSDK.authenticateStreamWebSocket(token?)` takes the
465
+ app's token (or the session's) and returns the boolean. The handshake carries no token any more
466
+ (`StreamWebSocketConfig.authToken` is gone; a static token was presented, expired, on every reconnect).
467
+ - **`deauthenticate()`** (`sdk.deauthenticateStreamWebSocket()`): the server has no message that drops a
468
+ socket's identity, so an authenticated socket is closed and opened again (subscriptions stay registered;
469
+ `onReconnect` fires). `setWallet` / `setWalletProvider` call it.
470
+ - **`joinTokenRoom(token)`** holds a token's room with no listener (the room stays joined while its
471
+ listeners come and go); it returns its own release.
472
+ - **The overseers' firehose** (`subscribeEventsFirehose(listener, filters?)`): one server subscription
473
+ for all subscribers, joined once the socket is authenticated (it is overseer-only) and again after every
474
+ re-authentication. The backend's firehose takes no filters, so each subscriber's
475
+ `{ eventTypes, tokenName, userAddress }` filters are applied client-side.
476
+ - **The comment room.** `COMMENT_*` events are sent to the comment room only; `subscribeCommentCreated`
477
+ (and the new `Updated` / `Deleted`) now emit `subscribe_comment` (it never did, so it received nothing).
478
+ - **User-room and overseer events are delivered to global subscribers** (`subscribeGlobalModeratorAdded`
479
+ / `Removed` / `RoleUpdated` / `InviteCreated`, `subscribeGlobalTokenBanned` / `Unbanned`,
480
+ `subscribeContentFlagged`, `subscribeFlagResolved`, `subscribeAIModerationComplete` / `Failed`,
481
+ `subscribeUserBanned` / `Unbanned`, `subscribeBanEnforcement`, `subscribeTradeUpdateNotify`,
482
+ `subscribeBalanceUpdated(token)`, ...), not only to a token room's subscribers.
483
+ - **Feed subscribers are served whether or not the token room is joined.** The backend sends a client in
484
+ both rooms one copy of an event; the service delivers it to the token subscribers and to the feed
485
+ subscribers (it used to deliver it to the token room only, never to the feed).
486
+ - **New events with typed payloads**: `STREAM_CHAT_UPDATED`, `STREAM_CHAT_DELETED`, `SLOW_MODE_CHANGED`,
487
+ `RECORDING_STATUS`, `RECORDINGS_COUNT_UPDATED`, `SIMULCAST_STATUS`, `DOWNLOAD_READY`,
488
+ `STREAM_CONTROL_STATUS_CHANGED`, `CONTENT_REACTION_ADDED` / `REMOVED`, `COMMENT_UPDATED` / `DELETED`,
489
+ `CONTENT_FLAGGED`, `FLAG_RESOLVED`, `AI_MODERATION_COMPLETE` / `FAILED`, `POOL_CREATED`,
490
+ `TRADE_UPDATE_NOTIFY`, `AI_MODERATION_GLOBAL_STATUS`, `EVENT` (firehose), `exception`.
491
+ `STREAM_COUNTDOWN_UPDATED` / `STREAM_LANGUAGE_UPDATED` are fed from `TOKEN_CONFIG_UPDATED` (the backend no
492
+ longer emits them separately). `getSiteConfigState()` returns the latest site-wide switches.
493
+ `subscribeFeedTokenEvents(cb)` hands every feed token event over named (`{ event, data }`).
494
+ - **A second copy of one occurrence is dropped** for the events the backend sends through two rooms a
495
+ client can be in (`CONTENT_FLAGGED`, `FLAG_RESOLVED`, `AI_MODERATION_*`, `POOL_CREATED`, `OVERSEER_*`):
496
+ within 5 seconds, by id.
497
+ - **A payload with no usable `tokenName` no longer throws inside the socket listener** (`data.tokenName.trim()`,
498
+ `data.content.slice`); it reaches the subscribers of the event for every token and none of a token.
499
+ - **The 30-second replay buffer is removed.** It replayed stale events (trade deltas, candles) to a
500
+ late subscriber as if they were new, and started a timer per event per token; registration now precedes
501
+ the join, so there is no join race left for it to cover.
502
+ - **Replaced APIs (9.0.0 is unpublished: no shims).** `setGlobalCallbacks` -> `addCallbacks(callbacks)`
503
+ (adds, returns the remover); `setGlobalFeedCallbacks`, `unsubscribeFromGlobalFeed` and the Promise form
504
+ of `subscribeToGlobalFeed` -> `subscribeToGlobalFeed(callbacks)` returns an unsubscribe;
505
+ `connectStreamWebSocket(callbacks)` adds subscribers; `subscribeToStream(token, callbacks)` adds, and
506
+ `unsubscribeFromStream(token)` removes only what `subscribeToStream` added. `subscribeEngagementStatsUpdated`
507
+ takes a token. `StreamingEventService` and its `streaming-events` types are deleted: the
508
+ `LaunchpadSDK.on*` methods they backed never fired (nothing fed them); they are now subscribers of the same
509
+ registry (`onStreamChatMessage`, `onViewerCountChanged`, `onUserBanned`, ...: every occurrence the socket
510
+ receives, for any token; join rooms with the `subscribe*` methods or `subscribeToStream`).
511
+ `disconnect()` releases every subscriber, listener and timer and rejects what is pending; the service can be
512
+ used again; `dispose()` (called by `sdk.cleanup()`) is final.
513
+ - **Payload types follow the backend**: `UserBannedEvent` / `UserUnbannedEvent` / `BanEnforcementEvent`
514
+ (`isGlobal`, `isPermanent`, optional `reason` / `expiresAt` / `userAddress`; `tokenName` is `null` for a
515
+ global ban; the type guards accept the banned user's own copy, which has no address),
516
+ `TradeExecutedEvent` (+ `symbol`, `icon`), `BalanceUpdatedEvent` (the backend sends no balances),
517
+ `StreamGlobalStatusEvent` / `StreamChatGlobalStatusEvent` (`updatedAt` optional: `{ enabled }`), and the
518
+ global-feed token types are the token-room types (the feed copies named other fields). `COMMENT_DELETED`
519
+ carries `commentId`, not `messageId`.
520
+
71
521
  ### Fixes
72
522
 
523
+ - `createSale`'s pre-flight refused launches the launch form and the deployed bundler accept (a
524
+ scheme-less link such as `t.me/x`, a lower-case symbol, a name of 2 or 21-25 characters, a
525
+ pre-buy of `5.`). It now refuses only what the bundler (fee window, bad words) or the chain
526
+ (`CreateTokenSaleDTO`, `TokenClass`) refuses: a name of letters and digits (any length), a symbol
527
+ of letters and digits in any case (the chain upper-cases it; the wire carries it as typed), no
528
+ link rules, a non-empty description (at most 1000) and image (at most 500), an integer
529
+ `saleStartTime`, and a plain-decimal pre-buy and curve portions, which are SIGNED in their
530
+ canonical spelling (`5.` -> `5`, `1.50` -> `1.5`: the chain keeps any other spelling a string
531
+ and refuses it). Bad words are still refused by the bundler after signing (it fetches the list).
532
+ `LaunchpadTradeInput`, `LaunchpadExtraFees` and `CreateSaleInput` are exported by name
533
+ (`@since 9.0.0`), and `LaunchpadTradeInput.extraFees` documents what is sent on a buy.
534
+ - `BundleSubmission.wait()` takes `{ signal?: AbortSignal }` beside `timeoutMs` / `pollIntervalMs`
535
+ (`@since 9.0.0`). An aborted wait rejects with `LaunchpadSDKError` `BUNDLE_WAIT_ABORTED`
536
+ (`details.method`, `details.bundleId`; an already aborted signal rejects before a socket is opened
537
+ or a read sent) and stops what it started: the in-flight read (the requestor gets the wait's
538
+ `AbortSignal` as `init.signal`), the pause between reads, the deadline timer, its socket listener
539
+ (an own socket is disconnected, a borrowed `bundleSocketFactory` socket never). Aborting stops
540
+ watching only; `attach` resumes the bundle. A wait that ends for any reason now stops its reads too
541
+ (they used to run on until the deadline, up to 10 minutes). Also: one failed confirmation read (an
542
+ HTTP error, a network error, an unreadable body) no longer rejects the wait with
543
+ `BUNDLE_REQUEST_FAILED` while the socket can still deliver: reads are repeated every
544
+ `pollIntervalMs` until the deadline, and only a verdict (PROCESSED / FAILED), the deadline or an
545
+ abort ends the wait. At the deadline after failing reads the error is `BUNDLE_CONFIRMATION_TIMEOUT`
546
+ with the last failed read in `details.lastReadFailure` (`{ message, status? }`); the deadline
547
+ also holds for a read that never answers.
73
548
  - `graduateToken` bought `calculateBuyAmountForGraduation`'s `amount` (the GALA cost of the
74
549
  remaining tokens) as the exact-buy token quantity. It buys `remainingTokens`, with the GALA cost
75
550
  as `expectedAmount` (the slippage cap).