@bropump/sdk 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @bropump/sdk
2
+
3
+ TypeScript SDK for the BroPump API.
4
+
5
+ ## Install
6
+
7
+ Install from public npmjs:
8
+
9
+ ```bash
10
+ npm install @bropump/sdk
11
+ ```
12
+
13
+ ## Example
14
+
15
+ ```ts
16
+ import { BroPumpClient } from '@bropump/sdk'
17
+
18
+ const client = new BroPumpClient({
19
+ network: 'mainnet',
20
+ })
21
+
22
+ const token = await client.tokens.create({
23
+ name: 'Bro Pump',
24
+ symbol: 'BRO',
25
+ description: 'Simple token flow',
26
+ deployer: 'YOUR_WALLET',
27
+ migrationThreshold: 35,
28
+ image: new Blob(['demo'], { type: 'image/png' }),
29
+ })
30
+
31
+ const ready = await client.tokens.waitForReadyToSign(token.id)
32
+
33
+ await client.tokens.submitSigned(token.id, {
34
+ submitId: ready.submit.id,
35
+ signedTransactions: ready.submit.transactions.map((tx) => tx),
36
+ })
37
+ ```
38
+
39
+ Use `network: 'devnet'` for the dev environment:
40
+
41
+ ```ts
42
+ const client = new BroPumpClient({
43
+ network: 'devnet',
44
+ })
45
+ ```
46
+
47
+ `baseUrl` remains available as an override for local or preview environments.
48
+
49
+ ## Publish
50
+
51
+ This package is set up to publish publicly to npmjs under `@bropump/sdk`.
52
+
53
+ ```bash
54
+ cd packages/sdk
55
+ npm publish --access public --registry=https://registry.npmjs.org
56
+ ```
@@ -0,0 +1,56 @@
1
+ import { BroPumpRealtimeStream } from './realtime.js';
2
+ import type { BroPumpClientOptions, BroPumpRequestOptions, BroPumpWaitOptions, BroPumpWatchOptions, BuildWithdrawRequest, BuildWithdrawView, CreateTokenInput, CurvePreview, FeePreviewRequest, HealthView, LaunchConfigView, SubmitAction, SubmitGoLiveInput, SubmitSignedTransactionsInput, SubmitTokenInput, TokenView, WalletBalanceView } from './types.js';
3
+ export declare class BroPumpClient {
4
+ readonly baseUrl: string;
5
+ readonly network: 'mainnet' | 'devnet';
6
+ private readonly defaultHeaders;
7
+ private readonly fetchImpl;
8
+ private readonly webSocketImpl;
9
+ readonly tokens: {
10
+ create: (input: CreateTokenInput, options?: BroPumpRequestOptions) => Promise<TokenView>;
11
+ get: (id: string, options?: BroPumpRequestOptions) => Promise<TokenView>;
12
+ submit: (id: string, input: SubmitTokenInput, options?: BroPumpRequestOptions) => Promise<TokenView>;
13
+ submitSigned: (id: string, input: SubmitSignedTransactionsInput, options?: BroPumpRequestOptions) => Promise<TokenView>;
14
+ goLive: (id: string, input: Omit<SubmitGoLiveInput, "action">, options?: BroPumpRequestOptions) => Promise<TokenView>;
15
+ watch: (id: string, options?: BroPumpWatchOptions) => Promise<BroPumpRealtimeStream<TokenView>>;
16
+ watchPool: (pool: string, options?: BroPumpWatchOptions) => Promise<BroPumpRealtimeStream<TokenView>>;
17
+ waitFor: (id: string, predicate: (token: TokenView) => boolean, options?: BroPumpWaitOptions) => Promise<TokenView>;
18
+ waitForReadyToSign: (id: string, options?: BroPumpWaitOptions) => Promise<TokenView & {
19
+ submit: SubmitAction;
20
+ }>;
21
+ waitForLive: (id: string, options?: BroPumpWaitOptions) => Promise<TokenView & {
22
+ status: "live";
23
+ }>;
24
+ };
25
+ readonly fees: {
26
+ preview: (input?: FeePreviewRequest, options?: BroPumpRequestOptions) => Promise<CurvePreview>;
27
+ };
28
+ readonly launchConfig: {
29
+ get: (options?: BroPumpRequestOptions) => Promise<LaunchConfigView>;
30
+ };
31
+ readonly experimental: {
32
+ wallet: {
33
+ getBalance: (address: string, options?: BroPumpRequestOptions) => Promise<WalletBalanceView>;
34
+ buildWithdraw: (input: BuildWithdrawRequest, options?: BroPumpRequestOptions) => Promise<BuildWithdrawView>;
35
+ };
36
+ };
37
+ constructor(options?: BroPumpClientOptions);
38
+ health(options?: BroPumpRequestOptions): Promise<HealthView>;
39
+ createToken(input: CreateTokenInput, options?: BroPumpRequestOptions): Promise<TokenView>;
40
+ getToken(id: string, options?: BroPumpRequestOptions): Promise<TokenView>;
41
+ submitToken(id: string, input: SubmitTokenInput, options?: BroPumpRequestOptions): Promise<TokenView>;
42
+ waitForToken(id: string, predicate: (token: TokenView) => boolean, options?: BroPumpWaitOptions): Promise<TokenView>;
43
+ waitForReadyToSign(id: string, options?: BroPumpWaitOptions): Promise<TokenView & {
44
+ submit: SubmitAction;
45
+ }>;
46
+ waitForLive(id: string, options?: BroPumpWaitOptions): Promise<TokenView & {
47
+ status: 'live';
48
+ }>;
49
+ watchToken(id: string, options?: BroPumpWatchOptions): Promise<BroPumpRealtimeStream<TokenView>>;
50
+ watchPool(pool: string, options?: BroPumpWatchOptions): Promise<BroPumpRealtimeStream<TokenView>>;
51
+ previewFees(input?: FeePreviewRequest, options?: BroPumpRequestOptions): Promise<CurvePreview>;
52
+ getLaunchConfig(options?: BroPumpRequestOptions): Promise<LaunchConfigView>;
53
+ getWalletBalance(address: string, options?: BroPumpRequestOptions): Promise<WalletBalanceView>;
54
+ buildWithdraw(input: BuildWithdrawRequest, options?: BroPumpRequestOptions): Promise<BuildWithdrawView>;
55
+ private request;
56
+ }
package/dist/client.js ADDED
@@ -0,0 +1,263 @@
1
+ import { BROPUMP_API_URL_BY_NETWORK, DEFAULT_BROPUMP_API_URL, DEFAULT_POLL_INTERVAL_MS, DEFAULT_WAIT_TIMEOUT_MS, } from './constants.js';
2
+ import { BroPumpApiError, BroPumpTokenFailedError } from './errors.js';
3
+ import { hasSubmitAction, isTokenFailed, isTokenLive } from './guards.js';
4
+ import { BroPumpRealtimeStream } from './realtime.js';
5
+ import { encodePathSegment, ensureNotAborted, isApiEnvelope, mergeHeaders, normalizeBaseUrl, sleep, } from './utils.js';
6
+ export class BroPumpClient {
7
+ baseUrl;
8
+ network;
9
+ defaultHeaders;
10
+ fetchImpl;
11
+ webSocketImpl;
12
+ tokens = {
13
+ create: (input, options) => this.createToken(input, options),
14
+ get: (id, options) => this.getToken(id, options),
15
+ submit: (id, input, options) => this.submitToken(id, input, options),
16
+ submitSigned: (id, input, options) => this.submitToken(id, input, options),
17
+ goLive: (id, input, options) => this.submitToken(id, { action: 'go_live', ...input }, options),
18
+ watch: (id, options) => this.watchToken(id, options),
19
+ watchPool: (pool, options) => this.watchPool(pool, options),
20
+ waitFor: (id, predicate, options) => this.waitForToken(id, predicate, options),
21
+ waitForReadyToSign: (id, options) => this.waitForReadyToSign(id, options),
22
+ waitForLive: (id, options) => this.waitForLive(id, options),
23
+ };
24
+ fees = {
25
+ preview: (input, options) => this.previewFees(input, options),
26
+ };
27
+ launchConfig = {
28
+ get: (options) => this.getLaunchConfig(options),
29
+ };
30
+ experimental = {
31
+ wallet: {
32
+ getBalance: (address, options) => this.getWalletBalance(address, options),
33
+ buildWithdraw: (input, options) => this.buildWithdraw(input, options),
34
+ },
35
+ };
36
+ constructor(options = {}) {
37
+ this.network = options.network || 'mainnet';
38
+ this.baseUrl = normalizeBaseUrl(options.baseUrl || BROPUMP_API_URL_BY_NETWORK[this.network] || DEFAULT_BROPUMP_API_URL);
39
+ this.defaultHeaders = options.headers;
40
+ this.fetchImpl = options.fetch;
41
+ this.webSocketImpl = options.WebSocket;
42
+ }
43
+ async health(options) {
44
+ return this.request({
45
+ path: '/health',
46
+ method: 'GET',
47
+ options,
48
+ });
49
+ }
50
+ async createToken(input, options) {
51
+ const form = buildCreateTokenFormData(input);
52
+ return this.request({
53
+ path: '/tokens/create',
54
+ method: 'POST',
55
+ body: form,
56
+ options,
57
+ });
58
+ }
59
+ async getToken(id, options) {
60
+ return this.request({
61
+ path: `/tokens/${encodePathSegment(id)}`,
62
+ method: 'GET',
63
+ options,
64
+ });
65
+ }
66
+ async submitToken(id, input, options) {
67
+ return this.request({
68
+ path: `/tokens/${encodePathSegment(id)}/submit`,
69
+ method: 'POST',
70
+ body: input,
71
+ options,
72
+ });
73
+ }
74
+ async waitForToken(id, predicate, options = {}) {
75
+ const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
76
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
77
+ const stopOnFailed = options.stopOnFailed ?? true;
78
+ const deadline = Date.now() + timeoutMs;
79
+ while (true) {
80
+ ensureNotAborted(options.signal);
81
+ const token = await this.getToken(id, options);
82
+ if (predicate(token)) {
83
+ return token;
84
+ }
85
+ if (stopOnFailed && isTokenFailed(token)) {
86
+ throw new BroPumpTokenFailedError(token);
87
+ }
88
+ const remainingMs = deadline - Date.now();
89
+ if (remainingMs <= 0) {
90
+ throw new Error(`Timed out waiting for token ${id}`);
91
+ }
92
+ await sleep(Math.min(pollIntervalMs, remainingMs), options.signal);
93
+ }
94
+ }
95
+ async waitForReadyToSign(id, options) {
96
+ const token = await this.waitForToken(id, hasSubmitAction, options);
97
+ if (!hasSubmitAction(token)) {
98
+ throw new Error(`Token ${id} is missing submit instructions`);
99
+ }
100
+ return token;
101
+ }
102
+ async waitForLive(id, options) {
103
+ const token = await this.waitForToken(id, isTokenLive, options);
104
+ if (!isTokenLive(token)) {
105
+ throw new Error(`Token ${id} did not reach live status`);
106
+ }
107
+ return token;
108
+ }
109
+ async watchToken(id, options) {
110
+ return BroPumpRealtimeStream.connect({
111
+ baseUrl: this.baseUrl,
112
+ pathname: `/tokens/${encodePathSegment(id)}/ws`,
113
+ WebSocket: this.webSocketImpl,
114
+ options,
115
+ });
116
+ }
117
+ async watchPool(pool, options) {
118
+ return BroPumpRealtimeStream.connect({
119
+ baseUrl: this.baseUrl,
120
+ pathname: `/pools/${encodePathSegment(pool)}/ws`,
121
+ WebSocket: this.webSocketImpl,
122
+ options,
123
+ });
124
+ }
125
+ async previewFees(input, options) {
126
+ return this.request({
127
+ path: '/fees/preview',
128
+ method: 'POST',
129
+ body: input ?? {},
130
+ options,
131
+ });
132
+ }
133
+ async getLaunchConfig(options) {
134
+ return this.request({
135
+ path: '/launch-config',
136
+ method: 'GET',
137
+ options,
138
+ });
139
+ }
140
+ async getWalletBalance(address, options) {
141
+ return this.request({
142
+ path: `/wallet/${encodePathSegment(address)}/balance`,
143
+ method: 'GET',
144
+ options,
145
+ });
146
+ }
147
+ async buildWithdraw(input, options) {
148
+ return this.request({
149
+ path: '/wallet/build-withdraw',
150
+ method: 'POST',
151
+ body: input,
152
+ options,
153
+ });
154
+ }
155
+ async request(params) {
156
+ const fetchImpl = this.fetchImpl ?? globalThis.fetch;
157
+ if (!fetchImpl) {
158
+ throw new Error('fetch is not available in this runtime');
159
+ }
160
+ const headers = mergeHeaders(this.defaultHeaders, params.options?.headers);
161
+ let body;
162
+ if (params.body instanceof FormData) {
163
+ body = params.body;
164
+ }
165
+ else if (params.body !== undefined) {
166
+ headers.set('Content-Type', 'application/json');
167
+ body = JSON.stringify(params.body);
168
+ }
169
+ const response = await fetchImpl(new URL(params.path, `${this.baseUrl}/`), {
170
+ method: params.method,
171
+ headers,
172
+ body,
173
+ signal: params.options?.signal,
174
+ });
175
+ const text = await response.text();
176
+ let parsed = null;
177
+ if (text) {
178
+ try {
179
+ parsed = JSON.parse(text);
180
+ }
181
+ catch {
182
+ parsed = text;
183
+ }
184
+ }
185
+ if (!response.ok) {
186
+ throw BroPumpApiError.fromResponse(response, parsed);
187
+ }
188
+ if (isApiEnvelope(parsed)) {
189
+ return parsed.data;
190
+ }
191
+ return parsed;
192
+ }
193
+ }
194
+ function buildCreateTokenFormData(input) {
195
+ const form = new FormData();
196
+ appendFormValue(form, 'mode', input.mode);
197
+ appendFormValue(form, 'name', input.name);
198
+ appendFormValue(form, 'symbol', input.symbol);
199
+ appendFormValue(form, 'description', input.description);
200
+ appendFormValue(form, 'deployer', input.deployer);
201
+ appendFormValue(form, 'migrationThreshold', input.migrationThreshold);
202
+ appendFormValue(form, 'firstBuyAmount', input.firstBuyAmount);
203
+ appendFormValue(form, 'x', input.x);
204
+ appendFormValue(form, 'website', input.website);
205
+ appendFormValue(form, 'telegram', input.telegram);
206
+ const image = toImagePart(input.image);
207
+ form.set('image', image.blob, image.filename);
208
+ return form;
209
+ }
210
+ function appendFormValue(form, key, value) {
211
+ if (value == null)
212
+ return;
213
+ form.set(key, String(value));
214
+ }
215
+ function toImagePart(image) {
216
+ if (isBlob(image)) {
217
+ return {
218
+ blob: image,
219
+ filename: defaultFilename(image.type),
220
+ };
221
+ }
222
+ if (image instanceof ArrayBuffer) {
223
+ return {
224
+ blob: new Blob([image], { type: 'image/png' }),
225
+ filename: 'token.png',
226
+ };
227
+ }
228
+ if (ArrayBuffer.isView(image)) {
229
+ return {
230
+ blob: new Blob([toOwnedArrayBuffer(image)], { type: 'image/png' }),
231
+ filename: 'token.png',
232
+ };
233
+ }
234
+ const filename = image.filename || defaultFilename(image.contentType);
235
+ const data = isBlob(image.data)
236
+ ? image.data
237
+ : new Blob([image.data], {
238
+ type: image.contentType || 'image/png',
239
+ });
240
+ return {
241
+ blob: data,
242
+ filename,
243
+ };
244
+ }
245
+ function defaultFilename(contentType) {
246
+ if (!contentType)
247
+ return 'token.png';
248
+ if (contentType.includes('jpeg'))
249
+ return 'token.jpg';
250
+ if (contentType.includes('webp'))
251
+ return 'token.webp';
252
+ if (contentType.includes('gif'))
253
+ return 'token.gif';
254
+ return 'token.png';
255
+ }
256
+ function isBlob(value) {
257
+ return typeof Blob !== 'undefined' && value instanceof Blob;
258
+ }
259
+ function toOwnedArrayBuffer(view) {
260
+ const bytes = new Uint8Array(view.byteLength);
261
+ bytes.set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
262
+ return bytes.buffer;
263
+ }
@@ -0,0 +1,7 @@
1
+ import type { BroPumpNetwork } from './types.js';
2
+ export declare const DEFAULT_BROPUMP_API_URL = "https://api.bropump.run";
3
+ export declare const DEFAULT_BROPUMP_DEVNET_API_URL = "https://dev.bropump.run";
4
+ export declare const DEFAULT_POLL_INTERVAL_MS = 1000;
5
+ export declare const DEFAULT_WAIT_TIMEOUT_MS = 120000;
6
+ export declare const DEFAULT_WS_OPEN_TIMEOUT_MS = 10000;
7
+ export declare const BROPUMP_API_URL_BY_NETWORK: Record<BroPumpNetwork, string>;
@@ -0,0 +1,9 @@
1
+ export const DEFAULT_BROPUMP_API_URL = 'https://api.bropump.run';
2
+ export const DEFAULT_BROPUMP_DEVNET_API_URL = 'https://dev.bropump.run';
3
+ export const DEFAULT_POLL_INTERVAL_MS = 1_000;
4
+ export const DEFAULT_WAIT_TIMEOUT_MS = 120_000;
5
+ export const DEFAULT_WS_OPEN_TIMEOUT_MS = 10_000;
6
+ export const BROPUMP_API_URL_BY_NETWORK = {
7
+ mainnet: DEFAULT_BROPUMP_API_URL,
8
+ devnet: DEFAULT_BROPUMP_DEVNET_API_URL,
9
+ };
@@ -0,0 +1,17 @@
1
+ import type { TokenView } from './types.js';
2
+ export declare class BroPumpApiError extends Error {
3
+ readonly status: number;
4
+ readonly code: string | null;
5
+ readonly body: unknown;
6
+ constructor(params: {
7
+ message: string;
8
+ status: number;
9
+ code?: string | null;
10
+ body?: unknown;
11
+ });
12
+ static fromResponse(response: Response, body: unknown): BroPumpApiError;
13
+ }
14
+ export declare class BroPumpTokenFailedError extends Error {
15
+ readonly token: TokenView;
16
+ constructor(token: TokenView);
17
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,32 @@
1
+ export class BroPumpApiError extends Error {
2
+ status;
3
+ code;
4
+ body;
5
+ constructor(params) {
6
+ super(params.message);
7
+ this.name = 'BroPumpApiError';
8
+ this.status = params.status;
9
+ this.code = params.code ?? null;
10
+ this.body = params.body ?? null;
11
+ }
12
+ static fromResponse(response, body) {
13
+ const payload = body;
14
+ const message = typeof payload?.error === 'string'
15
+ ? payload.error
16
+ : `Request failed with status ${response.status}`;
17
+ return new BroPumpApiError({
18
+ message,
19
+ status: response.status,
20
+ code: typeof payload?.code === 'string' ? payload.code : null,
21
+ body,
22
+ });
23
+ }
24
+ }
25
+ export class BroPumpTokenFailedError extends Error {
26
+ token;
27
+ constructor(token) {
28
+ super(token.error?.message || `Token ${token.id} failed`);
29
+ this.name = 'BroPumpTokenFailedError';
30
+ this.token = token;
31
+ }
32
+ }
@@ -0,0 +1,10 @@
1
+ import type { SubmitAction, TokenView } from './types.js';
2
+ export declare function hasSubmitAction(token: TokenView): token is TokenView & {
3
+ submit: SubmitAction;
4
+ };
5
+ export declare function isTokenFailed(token: TokenView): token is TokenView & {
6
+ status: 'failed';
7
+ };
8
+ export declare function isTokenLive(token: TokenView): token is TokenView & {
9
+ status: 'live';
10
+ };
package/dist/guards.js ADDED
@@ -0,0 +1,9 @@
1
+ export function hasSubmitAction(token) {
2
+ return Boolean(token.submit?.id && token.submit.transactions?.length);
3
+ }
4
+ export function isTokenFailed(token) {
5
+ return token.status === 'failed';
6
+ }
7
+ export function isTokenLive(token) {
8
+ return token.status === 'live';
9
+ }
@@ -0,0 +1,6 @@
1
+ export * from './client.js';
2
+ export * from './constants.js';
3
+ export * from './errors.js';
4
+ export * from './guards.js';
5
+ export * from './realtime.js';
6
+ export * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './client.js';
2
+ export * from './constants.js';
3
+ export * from './errors.js';
4
+ export * from './guards.js';
5
+ export * from './realtime.js';
6
+ export * from './types.js';
@@ -0,0 +1,28 @@
1
+ import type { BroPumpWatchOptions, RealtimeMessage } from './types.js';
2
+ export declare function buildWebSocketUrl(baseUrl: string, pathname: string): string;
3
+ export declare class BroPumpRealtimeStream<T> implements AsyncIterable<RealtimeMessage<T>> {
4
+ readonly messages: RealtimeMessage<T>[];
5
+ private readonly socket;
6
+ private readonly waiters;
7
+ private readonly pendingIterators;
8
+ private readonly queue;
9
+ private closedError;
10
+ private closedCleanly;
11
+ private manuallyClosed;
12
+ private constructor();
13
+ static connect<T>(params: {
14
+ baseUrl: string;
15
+ pathname: string;
16
+ WebSocket?: typeof globalThis.WebSocket;
17
+ options?: BroPumpWatchOptions;
18
+ }): Promise<BroPumpRealtimeStream<T>>;
19
+ waitFor(predicate: (message: RealtimeMessage<T>) => boolean, timeoutMs: number, fromIndex?: number): Promise<RealtimeMessage<T>>;
20
+ close(code?: number, reason?: string): void;
21
+ [Symbol.asyncIterator](): AsyncIterator<RealtimeMessage<T>>;
22
+ private next;
23
+ private enqueue;
24
+ private flushWaiters;
25
+ private fail;
26
+ private failWaiters;
27
+ private finishIterators;
28
+ }
@@ -0,0 +1,224 @@
1
+ import { DEFAULT_WS_OPEN_TIMEOUT_MS } from './constants.js';
2
+ import { createAbortError } from './utils.js';
3
+ export function buildWebSocketUrl(baseUrl, pathname) {
4
+ const url = new URL(pathname, `${baseUrl.replace(/\/+$/, '')}/`);
5
+ url.protocol = url.protocol === 'https:' || url.protocol === 'wss:' ? 'wss:' : 'ws:';
6
+ return url.toString();
7
+ }
8
+ export class BroPumpRealtimeStream {
9
+ messages = [];
10
+ socket;
11
+ waiters = new Set();
12
+ pendingIterators = new Set();
13
+ queue = [];
14
+ closedError = null;
15
+ closedCleanly = false;
16
+ manuallyClosed = false;
17
+ constructor(socket) {
18
+ this.socket = socket;
19
+ this.socket.addEventListener('message', (event) => {
20
+ try {
21
+ const parsed = JSON.parse(String(event.data));
22
+ this.messages.push(parsed);
23
+ this.enqueue(parsed);
24
+ this.flushWaiters();
25
+ }
26
+ catch (error) {
27
+ this.fail(error instanceof Error ? error : new Error('Failed parsing realtime payload'));
28
+ }
29
+ });
30
+ this.socket.addEventListener('close', (event) => {
31
+ const error = this.manuallyClosed || event.code === 1000
32
+ ? null
33
+ : new Error(`WebSocket closed${event.code ? ` (${event.code})` : ''}${event.reason ? `: ${event.reason}` : ''}`);
34
+ if (error) {
35
+ this.fail(error);
36
+ return;
37
+ }
38
+ this.closedCleanly = true;
39
+ this.failWaiters(new Error('WebSocket closed'));
40
+ this.finishIterators();
41
+ });
42
+ this.socket.addEventListener('error', () => {
43
+ this.fail(new Error('WebSocket error'));
44
+ });
45
+ }
46
+ static async connect(params) {
47
+ const WebSocketCtor = params.WebSocket ?? globalThis.WebSocket;
48
+ if (!WebSocketCtor) {
49
+ throw new Error('WebSocket is not available in this runtime');
50
+ }
51
+ const url = buildWebSocketUrl(params.baseUrl, params.pathname);
52
+ const socket = new WebSocketCtor(url);
53
+ const timeoutMs = params.options?.timeoutMs ?? DEFAULT_WS_OPEN_TIMEOUT_MS;
54
+ await new Promise((resolve, reject) => {
55
+ let settled = false;
56
+ const timeout = setTimeout(() => {
57
+ cleanup();
58
+ settled = true;
59
+ try {
60
+ socket.close(1000, 'open timeout');
61
+ }
62
+ catch {
63
+ // no-op
64
+ }
65
+ reject(new Error(`Timed out opening WebSocket ${url}`));
66
+ }, timeoutMs);
67
+ const onAbort = () => {
68
+ if (settled)
69
+ return;
70
+ cleanup();
71
+ settled = true;
72
+ try {
73
+ socket.close(1000, 'aborted');
74
+ }
75
+ catch {
76
+ // no-op
77
+ }
78
+ reject(createAbortError());
79
+ };
80
+ const onOpen = () => {
81
+ if (settled)
82
+ return;
83
+ cleanup();
84
+ settled = true;
85
+ resolve();
86
+ };
87
+ const onError = () => {
88
+ if (settled)
89
+ return;
90
+ cleanup();
91
+ settled = true;
92
+ reject(new Error(`Failed opening WebSocket ${url}`));
93
+ };
94
+ const onClose = (event) => {
95
+ if (settled)
96
+ return;
97
+ cleanup();
98
+ settled = true;
99
+ const closeEvent = event;
100
+ reject(new Error(`WebSocket closed before open${closeEvent.code ? ` (${closeEvent.code})` : ''}`));
101
+ };
102
+ const cleanup = () => {
103
+ clearTimeout(timeout);
104
+ params.options?.signal?.removeEventListener('abort', onAbort);
105
+ socket.removeEventListener('open', onOpen);
106
+ socket.removeEventListener('error', onError);
107
+ socket.removeEventListener('close', onClose);
108
+ };
109
+ params.options?.signal?.addEventListener('abort', onAbort, { once: true });
110
+ socket.addEventListener('open', onOpen, { once: true });
111
+ socket.addEventListener('error', onError, { once: true });
112
+ socket.addEventListener('close', onClose, { once: true });
113
+ });
114
+ return new BroPumpRealtimeStream(socket);
115
+ }
116
+ async waitFor(predicate, timeoutMs, fromIndex = 0) {
117
+ const existing = this.messages.slice(fromIndex).find(predicate);
118
+ if (existing)
119
+ return existing;
120
+ if (this.closedError)
121
+ throw this.closedError;
122
+ if (this.closedCleanly) {
123
+ throw new Error('WebSocket closed');
124
+ }
125
+ return new Promise((resolve, reject) => {
126
+ const waiter = {
127
+ predicate,
128
+ resolve: (message) => {
129
+ clearTimeout(waiter.timeout);
130
+ this.waiters.delete(waiter);
131
+ resolve(message);
132
+ },
133
+ reject: (error) => {
134
+ clearTimeout(waiter.timeout);
135
+ this.waiters.delete(waiter);
136
+ reject(error);
137
+ },
138
+ timeout: setTimeout(() => {
139
+ this.waiters.delete(waiter);
140
+ reject(new Error('Timed out waiting for realtime message'));
141
+ }, timeoutMs),
142
+ fromIndex,
143
+ };
144
+ this.waiters.add(waiter);
145
+ });
146
+ }
147
+ close(code, reason) {
148
+ this.manuallyClosed = true;
149
+ try {
150
+ this.socket.close(code, reason);
151
+ }
152
+ catch {
153
+ // no-op
154
+ }
155
+ }
156
+ [Symbol.asyncIterator]() {
157
+ return {
158
+ next: () => this.next(),
159
+ };
160
+ }
161
+ next() {
162
+ if (this.queue.length > 0) {
163
+ return Promise.resolve({
164
+ value: this.queue.shift(),
165
+ done: false,
166
+ });
167
+ }
168
+ if (this.closedError) {
169
+ return Promise.reject(this.closedError);
170
+ }
171
+ if (this.closedCleanly) {
172
+ return Promise.resolve({
173
+ value: undefined,
174
+ done: true,
175
+ });
176
+ }
177
+ return new Promise((resolve, reject) => {
178
+ const pending = { resolve, reject };
179
+ this.pendingIterators.add(pending);
180
+ });
181
+ }
182
+ enqueue(message) {
183
+ const pending = this.pendingIterators.values().next().value;
184
+ if (pending) {
185
+ this.pendingIterators.delete(pending);
186
+ pending.resolve({
187
+ value: message,
188
+ done: false,
189
+ });
190
+ return;
191
+ }
192
+ this.queue.push(message);
193
+ }
194
+ flushWaiters() {
195
+ for (const waiter of Array.from(this.waiters)) {
196
+ const message = this.messages.slice(waiter.fromIndex).find(waiter.predicate);
197
+ if (message) {
198
+ waiter.resolve(message);
199
+ }
200
+ }
201
+ }
202
+ fail(error) {
203
+ this.closedError = this.closedError || error;
204
+ this.failWaiters(this.closedError);
205
+ for (const pending of Array.from(this.pendingIterators)) {
206
+ this.pendingIterators.delete(pending);
207
+ pending.reject(this.closedError);
208
+ }
209
+ }
210
+ failWaiters(error) {
211
+ for (const waiter of Array.from(this.waiters)) {
212
+ waiter.reject(error);
213
+ }
214
+ }
215
+ finishIterators() {
216
+ for (const pending of Array.from(this.pendingIterators)) {
217
+ this.pendingIterators.delete(pending);
218
+ pending.resolve({
219
+ value: undefined,
220
+ done: true,
221
+ });
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,224 @@
1
+ export type ApiEnvelope<T> = {
2
+ success: true;
3
+ data: T;
4
+ };
5
+ export type BroPumpErrorResponse = {
6
+ error: string;
7
+ code: string;
8
+ };
9
+ export type BroPumpNetwork = 'mainnet' | 'devnet';
10
+ export type BroPumpClientOptions = {
11
+ network?: BroPumpNetwork;
12
+ baseUrl?: string;
13
+ fetch?: typeof globalThis.fetch;
14
+ WebSocket?: typeof globalThis.WebSocket;
15
+ headers?: HeadersInit;
16
+ };
17
+ export type BroPumpRequestOptions = {
18
+ signal?: AbortSignal;
19
+ headers?: HeadersInit;
20
+ };
21
+ export type BroPumpWaitOptions = BroPumpRequestOptions & {
22
+ timeoutMs?: number;
23
+ pollIntervalMs?: number;
24
+ stopOnFailed?: boolean;
25
+ };
26
+ export type BroPumpWatchOptions = {
27
+ timeoutMs?: number;
28
+ signal?: AbortSignal;
29
+ };
30
+ export type UploadableImage = Blob | ArrayBuffer | ArrayBufferView | {
31
+ data: BlobPart | Blob;
32
+ filename?: string;
33
+ contentType?: string;
34
+ };
35
+ export type CreateTokenInput = {
36
+ mode?: 'live' | 'offchain';
37
+ name: string;
38
+ symbol: string;
39
+ description: string;
40
+ deployer: string;
41
+ migrationThreshold?: number;
42
+ firstBuyAmount?: number;
43
+ x?: string;
44
+ website?: string;
45
+ telegram?: string;
46
+ image: UploadableImage;
47
+ };
48
+ export type SubmitSignedTransactionsInput = {
49
+ submitId: string;
50
+ signedTransactions: string[];
51
+ };
52
+ export type SubmitGoLiveInput = {
53
+ action: 'go_live';
54
+ wallet: string;
55
+ firstBuyAmount?: number;
56
+ };
57
+ export type SubmitTokenInput = SubmitSignedTransactionsInput | SubmitGoLiveInput;
58
+ export type HealthView = {
59
+ ok: boolean;
60
+ role: string;
61
+ api?: string;
62
+ };
63
+ export type LaunchCostBreakdown = {
64
+ baseTxFeesSol: number;
65
+ priorityFeesSol: number;
66
+ senderTipsSol: number;
67
+ firstBuyAmountSol: number;
68
+ otherLamportSpendsSol: number;
69
+ bufferSol: number;
70
+ };
71
+ export type LaunchCostPhaseQuote = {
72
+ phase: 'tx1' | 'tx2';
73
+ mode: 'estimated' | 'exact';
74
+ estimatedChainCostSol: number;
75
+ breakdown: Omit<LaunchCostBreakdown, 'bufferSol'>;
76
+ };
77
+ export type LaunchCostQuote = {
78
+ currency: 'SOL';
79
+ generatedAt: string;
80
+ validUntil: string;
81
+ estimatedTotalChainCostSol: number;
82
+ recommendedMinBalanceSol: number;
83
+ remainingChainCostSol: number;
84
+ walletBalanceSol: number | null;
85
+ sufficientBalance: boolean | null;
86
+ shortfallSol: number | null;
87
+ phases: {
88
+ tx1: LaunchCostPhaseQuote;
89
+ tx2?: LaunchCostPhaseQuote;
90
+ };
91
+ breakdown: LaunchCostBreakdown;
92
+ };
93
+ export type MigrationThresholdOption = {
94
+ migrationThreshold: number;
95
+ presetBinding: string;
96
+ configAddress: string | null;
97
+ deploymentMode: 'predeployed-config' | 'per-launch-config';
98
+ };
99
+ export type LaunchConfigView = {
100
+ network: 'devnet' | 'mainnet';
101
+ defaultMigrationThreshold: number;
102
+ allowedMigrationThresholds: number[];
103
+ availableMigrationThresholds: number[];
104
+ migrationThresholdOptions: MigrationThresholdOption[];
105
+ availableMigrationThresholdOptions: MigrationThresholdOption[];
106
+ };
107
+ export type SubmitAction = {
108
+ id: string;
109
+ phase: 'tx1' | 'tx2';
110
+ transactions: string[];
111
+ expiresAt: string | null;
112
+ };
113
+ export type TokenRuntime = {
114
+ mintAddress: string;
115
+ poolAddress: string | null;
116
+ configAddress: string | null;
117
+ launchId: string | null;
118
+ phase: 'offchain' | 'live' | 'migrated';
119
+ isTrading: boolean;
120
+ isMigrated: boolean;
121
+ completionPercent: number | null;
122
+ migrationTargetQuote: number | null;
123
+ currentQuoteReserve: number | null;
124
+ updatedAt: string;
125
+ };
126
+ export type TokenMarket = {
127
+ holderCount: number | null;
128
+ isVerified: boolean | null;
129
+ usdPrice: number | null;
130
+ mcap: number | null;
131
+ volume24h: number | null;
132
+ updatedAt: string;
133
+ };
134
+ export type TokenStatus = 'processing' | 'offchain_ready' | 'ready_to_sign' | 'submitting' | 'needs_resign' | 'live' | 'failed';
135
+ export type TokenView = {
136
+ id: string;
137
+ mode: 'live' | 'offchain';
138
+ status: TokenStatus;
139
+ name: string;
140
+ symbol: string;
141
+ imageUrl: string | null;
142
+ mint: string | null;
143
+ pool: string | null;
144
+ submit?: SubmitAction;
145
+ runtime?: TokenRuntime | null;
146
+ market?: TokenMarket | null;
147
+ error?: {
148
+ code: string | null;
149
+ message: string | null;
150
+ };
151
+ createdAt: string;
152
+ updatedAt: string;
153
+ liveAt: string | null;
154
+ expiresAt: string | null;
155
+ };
156
+ export type RealtimeMessage<T> = {
157
+ type: string;
158
+ at: string;
159
+ data: T | null;
160
+ };
161
+ export type CurvePreview = {
162
+ allowedMigrationThresholds: number[];
163
+ migrationThresholdOptions: MigrationThresholdOption[];
164
+ selectedMigrationThreshold: number;
165
+ curve: {
166
+ startingMC: number;
167
+ endingMC: number;
168
+ migrationThreshold: number;
169
+ migrationThresholdUSD: number;
170
+ solPrice: number;
171
+ };
172
+ realMarketCap: {
173
+ startQuote: number;
174
+ startUSD: number;
175
+ migrationQuote: number;
176
+ migrationUSD: number;
177
+ };
178
+ priceRange: {
179
+ startQuotePerToken: number;
180
+ startUSDPerToken: number;
181
+ migrationQuotePerToken: number;
182
+ migrationUSDPerToken: number;
183
+ };
184
+ lockedLiquidity: {
185
+ lockedPercent: number;
186
+ baseLocked: number;
187
+ quoteLocked: number;
188
+ quoteLockedUSD: number;
189
+ };
190
+ raiseSummary: {
191
+ targetQuote: number;
192
+ targetUSD: number;
193
+ curveQuoteAfterTradingFee: number;
194
+ curveUsdAfterTradingFee: number;
195
+ deepLiquidityQuote: number;
196
+ deepLiquidityUSD: number;
197
+ lpTokenPercent: number;
198
+ lpTokenAmount: number;
199
+ lpLockedPercent: number;
200
+ };
201
+ costQuote: LaunchCostQuote;
202
+ };
203
+ export type FeePreviewRequest = {
204
+ totalSupply?: string;
205
+ migrationThreshold?: number;
206
+ firstBuyAmount?: number;
207
+ deployerWallet?: string;
208
+ };
209
+ export type WalletBalanceView = {
210
+ address: string;
211
+ sol: number;
212
+ rentReserveLamports: number;
213
+ maxWithdrawSol: number;
214
+ };
215
+ export type BuildWithdrawRequest = {
216
+ address: string;
217
+ destination: string;
218
+ amountSol: number;
219
+ };
220
+ export type BuildWithdrawView = {
221
+ transaction: string;
222
+ lamports: number;
223
+ maxWithdrawable: number;
224
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { ApiEnvelope } from './types.js';
2
+ export declare function normalizeBaseUrl(baseUrl: string): string;
3
+ export declare function mergeHeaders(...sets: Array<HeadersInit | undefined>): Headers;
4
+ export declare function isApiEnvelope<T>(value: unknown): value is ApiEnvelope<T>;
5
+ export declare function encodePathSegment(value: string): string;
6
+ export declare function createAbortError(): Error;
7
+ export declare function ensureNotAborted(signal?: AbortSignal): void;
8
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
package/dist/utils.js ADDED
@@ -0,0 +1,57 @@
1
+ export function normalizeBaseUrl(baseUrl) {
2
+ return baseUrl.replace(/\/+$/, '');
3
+ }
4
+ export function mergeHeaders(...sets) {
5
+ const headers = new Headers();
6
+ for (const set of sets) {
7
+ if (!set)
8
+ continue;
9
+ new Headers(set).forEach((value, key) => {
10
+ headers.set(key, value);
11
+ });
12
+ }
13
+ return headers;
14
+ }
15
+ export function isApiEnvelope(value) {
16
+ if (!value || typeof value !== 'object')
17
+ return false;
18
+ return 'success' in value && 'data' in value;
19
+ }
20
+ export function encodePathSegment(value) {
21
+ return encodeURIComponent(value);
22
+ }
23
+ export function createAbortError() {
24
+ try {
25
+ return new DOMException('The operation was aborted', 'AbortError');
26
+ }
27
+ catch {
28
+ const error = new Error('The operation was aborted');
29
+ error.name = 'AbortError';
30
+ return error;
31
+ }
32
+ }
33
+ export function ensureNotAborted(signal) {
34
+ if (signal?.aborted) {
35
+ throw createAbortError();
36
+ }
37
+ }
38
+ export async function sleep(ms, signal) {
39
+ ensureNotAborted(signal);
40
+ if (ms <= 0)
41
+ return;
42
+ await new Promise((resolve, reject) => {
43
+ const timer = setTimeout(() => {
44
+ cleanup();
45
+ resolve();
46
+ }, ms);
47
+ const onAbort = () => {
48
+ cleanup();
49
+ reject(createAbortError());
50
+ };
51
+ const cleanup = () => {
52
+ clearTimeout(timer);
53
+ signal?.removeEventListener('abort', onAbort);
54
+ };
55
+ signal?.addEventListener('abort', onAbort, { once: true });
56
+ });
57
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@bropump/sdk",
3
+ "version": "0.2.1",
4
+ "description": "TypeScript SDK for the BroPump API",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/bropump/bropumplauncher.git",
17
+ "directory": "packages/sdk"
18
+ },
19
+ "keywords": [
20
+ "bropump",
21
+ "sdk",
22
+ "typescript",
23
+ "api",
24
+ "cloudflare"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js"
33
+ }
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "registry": "https://registry.npmjs.org/"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.build.json",
41
+ "typecheck": "tsc -p tsconfig.build.json --noEmit",
42
+ "test": "bun test ./tests/*.test.ts",
43
+ "prepack": "tsc -p tsconfig.build.json"
44
+ }
45
+ }