@gala-chain/launchpad-sdk 9.0.0-rc.0 → 9.0.0-rc.2

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