@marinade.finance/ts-subscription-client 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ import {
2
+ subscribeMessage,
3
+ unsubscribeMessage,
4
+ listSubscriptionsMessage,
5
+ } from '../index'
6
+
7
+ describe('message helpers', () => {
8
+ it('subscribeMessage', () => {
9
+ expect(subscribeMessage('sam_auction', 'telegram', 1710000000)).toBe(
10
+ 'Subscribe sam_auction telegram 1710000000',
11
+ )
12
+ })
13
+
14
+ it('unsubscribeMessage', () => {
15
+ expect(unsubscribeMessage('sam_auction', 'email', 1710000000)).toBe(
16
+ 'Unsubscribe sam_auction email 1710000000',
17
+ )
18
+ })
19
+
20
+ it('listSubscriptionsMessage', () => {
21
+ expect(
22
+ listSubscriptionsMessage(
23
+ 'GrxB8UaaaaaaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAAAA',
24
+ 1710000000,
25
+ ),
26
+ ).toBe(
27
+ 'ListSubscriptions GrxB8UaaaaaaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAAAA 1710000000',
28
+ )
29
+ })
30
+ })
package/client.ts ADDED
@@ -0,0 +1,158 @@
1
+ import {
2
+ type SubscriptionClientConfig,
3
+ type Logger,
4
+ type SubscribeRequest,
5
+ type SubscribeResponse,
6
+ type UnsubscribeRequest,
7
+ type UnsubscribeResponse,
8
+ type ListSubscriptionsQuery,
9
+ type ListSubscriptionsAuth,
10
+ type Subscription,
11
+ type ListNotificationsQuery,
12
+ type Notification,
13
+ NetworkError,
14
+ } from './types'
15
+
16
+ const PATH_SUBSCRIPTIONS = '/subscriptions'
17
+ const PATH_NOTIFICATIONS = '/notifications'
18
+
19
+ export class SubscriptionClient {
20
+ private readonly base_url: string
21
+ private readonly timeout_ms: number
22
+ private readonly logger?: Logger
23
+
24
+ constructor(config: SubscriptionClientConfig) {
25
+ this.base_url = config.base_url
26
+ this.timeout_ms = config.timeout_ms ?? 10_000
27
+ this.logger = config.logger
28
+ }
29
+
30
+ async subscribe(request: SubscribeRequest): Promise<SubscribeResponse> {
31
+ this.logger?.debug(
32
+ `POST ${PATH_SUBSCRIPTIONS} body: ${JSON.stringify({
33
+ ...request,
34
+ signature: '[redacted]',
35
+ })}`,
36
+ )
37
+ return this.fetch<SubscribeResponse>(PATH_SUBSCRIPTIONS, {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify(request),
41
+ })
42
+ }
43
+
44
+ async unsubscribe(request: UnsubscribeRequest): Promise<UnsubscribeResponse> {
45
+ this.logger?.debug(
46
+ `DELETE ${PATH_SUBSCRIPTIONS} body: ${JSON.stringify({
47
+ ...request,
48
+ signature: '[redacted]',
49
+ })}`,
50
+ )
51
+ return this.fetch<UnsubscribeResponse>(PATH_SUBSCRIPTIONS, {
52
+ method: 'DELETE',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify(request),
55
+ })
56
+ }
57
+
58
+ async listSubscriptions(
59
+ query: ListSubscriptionsQuery,
60
+ auth: ListSubscriptionsAuth,
61
+ ): Promise<Subscription[]> {
62
+ const params = new URLSearchParams()
63
+ params.set('pubkey', query.pubkey)
64
+ if (query.notification_type) {
65
+ params.set('notification_type', query.notification_type)
66
+ }
67
+ if (
68
+ query.additional_data &&
69
+ Object.keys(query.additional_data).length > 0
70
+ ) {
71
+ params.set('additional_data', JSON.stringify(query.additional_data))
72
+ }
73
+ const path = `${PATH_SUBSCRIPTIONS}?${params.toString()}`
74
+
75
+ this.logger?.debug(`GET ${path} signature: [redacted]`)
76
+ return this.fetch<Subscription[]>(path, {
77
+ method: 'GET',
78
+ headers: {
79
+ 'x-solana-signature': auth.signature,
80
+ 'x-solana-message': auth.message,
81
+ },
82
+ })
83
+ }
84
+
85
+ async listNotifications(
86
+ query: ListNotificationsQuery,
87
+ ): Promise<Notification[]> {
88
+ const params = new URLSearchParams()
89
+ params.set('user_id', query.user_id)
90
+ if (query.notification_type) {
91
+ params.set('notification_type', query.notification_type)
92
+ }
93
+ if (query.priority) {
94
+ params.set('priority', query.priority)
95
+ }
96
+ if (query.inner_type) {
97
+ params.set('inner_type', query.inner_type)
98
+ }
99
+ if (query.limit !== undefined) {
100
+ params.set('limit', query.limit.toString())
101
+ }
102
+ if (query.offset !== undefined) {
103
+ params.set('offset', query.offset.toString())
104
+ }
105
+ const path = `${PATH_NOTIFICATIONS}?${params.toString()}`
106
+
107
+ this.logger?.debug(`GET ${path}`)
108
+ return this.fetch<Notification[]>(path, {
109
+ method: 'GET',
110
+ })
111
+ }
112
+
113
+ private async fetch<T>(path: string, init: RequestInit): Promise<T> {
114
+ const url = `${this.base_url}${path}`
115
+ const method = init.method ?? 'GET'
116
+
117
+ const controller = new AbortController()
118
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout_ms)
119
+
120
+ try {
121
+ const response = await globalThis.fetch(url, {
122
+ ...init,
123
+ signal: controller.signal,
124
+ })
125
+
126
+ clearTimeout(timeoutId)
127
+
128
+ if (!response.ok) {
129
+ const errorBody = await response.text().catch(() => 'unknown error')
130
+ throw new NetworkError(
131
+ `${method} ${path} failed: ` +
132
+ `${response.status} ${response.statusText}`,
133
+ response.status,
134
+ errorBody,
135
+ )
136
+ }
137
+
138
+ return (await response.json()) as T
139
+ } catch (error) {
140
+ clearTimeout(timeoutId)
141
+
142
+ if (error instanceof NetworkError) {
143
+ throw error
144
+ }
145
+
146
+ if (error instanceof Error) {
147
+ if (error.name === 'AbortError') {
148
+ throw new NetworkError(
149
+ `${method} ${path} timeout after ${this.timeout_ms}ms`,
150
+ )
151
+ }
152
+ throw new NetworkError(`${method} ${path} failed: ${error.message}`)
153
+ }
154
+
155
+ throw new NetworkError(`${method} ${path} failed: unknown error`)
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,12 @@
1
+ import { type SubscriptionClientConfig, type SubscribeRequest, type SubscribeResponse, type UnsubscribeRequest, type UnsubscribeResponse, type ListSubscriptionsQuery, type ListSubscriptionsAuth, type Subscription, type ListNotificationsQuery, type Notification } from './types';
2
+ export declare class SubscriptionClient {
3
+ private readonly base_url;
4
+ private readonly timeout_ms;
5
+ private readonly logger?;
6
+ constructor(config: SubscriptionClientConfig);
7
+ subscribe(request: SubscribeRequest): Promise<SubscribeResponse>;
8
+ unsubscribe(request: UnsubscribeRequest): Promise<UnsubscribeResponse>;
9
+ listSubscriptions(query: ListSubscriptionsQuery, auth: ListSubscriptionsAuth): Promise<Subscription[]>;
10
+ listNotifications(query: ListNotificationsQuery): Promise<Notification[]>;
11
+ private fetch;
12
+ }
package/dist/client.js ADDED
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SubscriptionClient = void 0;
4
+ const types_1 = require("./types");
5
+ const PATH_SUBSCRIPTIONS = '/subscriptions';
6
+ const PATH_NOTIFICATIONS = '/notifications';
7
+ class SubscriptionClient {
8
+ constructor(config) {
9
+ this.base_url = config.base_url;
10
+ this.timeout_ms = config.timeout_ms ?? 10000;
11
+ this.logger = config.logger;
12
+ }
13
+ async subscribe(request) {
14
+ this.logger?.debug(`POST ${PATH_SUBSCRIPTIONS} body: ${JSON.stringify({
15
+ ...request,
16
+ signature: '[redacted]',
17
+ })}`);
18
+ return this.fetch(PATH_SUBSCRIPTIONS, {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify(request),
22
+ });
23
+ }
24
+ async unsubscribe(request) {
25
+ this.logger?.debug(`DELETE ${PATH_SUBSCRIPTIONS} body: ${JSON.stringify({
26
+ ...request,
27
+ signature: '[redacted]',
28
+ })}`);
29
+ return this.fetch(PATH_SUBSCRIPTIONS, {
30
+ method: 'DELETE',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify(request),
33
+ });
34
+ }
35
+ async listSubscriptions(query, auth) {
36
+ const params = new URLSearchParams();
37
+ params.set('pubkey', query.pubkey);
38
+ if (query.notification_type) {
39
+ params.set('notification_type', query.notification_type);
40
+ }
41
+ if (query.additional_data &&
42
+ Object.keys(query.additional_data).length > 0) {
43
+ params.set('additional_data', JSON.stringify(query.additional_data));
44
+ }
45
+ const path = `${PATH_SUBSCRIPTIONS}?${params.toString()}`;
46
+ this.logger?.debug(`GET ${path} signature: [redacted]`);
47
+ return this.fetch(path, {
48
+ method: 'GET',
49
+ headers: {
50
+ 'x-solana-signature': auth.signature,
51
+ 'x-solana-message': auth.message,
52
+ },
53
+ });
54
+ }
55
+ async listNotifications(query) {
56
+ const params = new URLSearchParams();
57
+ params.set('user_id', query.user_id);
58
+ if (query.notification_type) {
59
+ params.set('notification_type', query.notification_type);
60
+ }
61
+ if (query.priority) {
62
+ params.set('priority', query.priority);
63
+ }
64
+ if (query.inner_type) {
65
+ params.set('inner_type', query.inner_type);
66
+ }
67
+ if (query.limit !== undefined) {
68
+ params.set('limit', query.limit.toString());
69
+ }
70
+ if (query.offset !== undefined) {
71
+ params.set('offset', query.offset.toString());
72
+ }
73
+ const path = `${PATH_NOTIFICATIONS}?${params.toString()}`;
74
+ this.logger?.debug(`GET ${path}`);
75
+ return this.fetch(path, {
76
+ method: 'GET',
77
+ });
78
+ }
79
+ async fetch(path, init) {
80
+ const url = `${this.base_url}${path}`;
81
+ const method = init.method ?? 'GET';
82
+ const controller = new AbortController();
83
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout_ms);
84
+ try {
85
+ const response = await globalThis.fetch(url, {
86
+ ...init,
87
+ signal: controller.signal,
88
+ });
89
+ clearTimeout(timeoutId);
90
+ if (!response.ok) {
91
+ const errorBody = await response.text().catch(() => 'unknown error');
92
+ throw new types_1.NetworkError(`${method} ${path} failed: ` +
93
+ `${response.status} ${response.statusText}`, response.status, errorBody);
94
+ }
95
+ return (await response.json());
96
+ }
97
+ catch (error) {
98
+ clearTimeout(timeoutId);
99
+ if (error instanceof types_1.NetworkError) {
100
+ throw error;
101
+ }
102
+ if (error instanceof Error) {
103
+ if (error.name === 'AbortError') {
104
+ throw new types_1.NetworkError(`${method} ${path} timeout after ${this.timeout_ms}ms`);
105
+ }
106
+ throw new types_1.NetworkError(`${method} ${path} failed: ${error.message}`);
107
+ }
108
+ throw new types_1.NetworkError(`${method} ${path} failed: unknown error`);
109
+ }
110
+ }
111
+ }
112
+ exports.SubscriptionClient = SubscriptionClient;
@@ -0,0 +1,8 @@
1
+ import { SubscriptionClient } from './client';
2
+ import type { SubscriptionClientConfig } from './types';
3
+ export { SubscriptionClient };
4
+ export { subscribeMessage, unsubscribeMessage, listSubscriptionsMessage, } from './message';
5
+ export { NetworkError } from './types';
6
+ export type { Logger, SubscriptionClientConfig, SubscribeRequest, SubscribeResponse, UnsubscribeRequest, UnsubscribeResponse, ListSubscriptionsQuery, ListSubscriptionsAuth, Subscription, ListNotificationsQuery, Notification, } from './types';
7
+ export declare const NOTIFICATION_TYPE_SAM_AUCTION = "sam_auction";
8
+ export declare function createSubscriptionClient(config: SubscriptionClientConfig): SubscriptionClient;
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NOTIFICATION_TYPE_SAM_AUCTION = exports.NetworkError = exports.listSubscriptionsMessage = exports.unsubscribeMessage = exports.subscribeMessage = exports.SubscriptionClient = void 0;
4
+ exports.createSubscriptionClient = createSubscriptionClient;
5
+ const client_1 = require("./client");
6
+ Object.defineProperty(exports, "SubscriptionClient", { enumerable: true, get: function () { return client_1.SubscriptionClient; } });
7
+ var message_1 = require("./message");
8
+ Object.defineProperty(exports, "subscribeMessage", { enumerable: true, get: function () { return message_1.subscribeMessage; } });
9
+ Object.defineProperty(exports, "unsubscribeMessage", { enumerable: true, get: function () { return message_1.unsubscribeMessage; } });
10
+ Object.defineProperty(exports, "listSubscriptionsMessage", { enumerable: true, get: function () { return message_1.listSubscriptionsMessage; } });
11
+ var types_1 = require("./types");
12
+ Object.defineProperty(exports, "NetworkError", { enumerable: true, get: function () { return types_1.NetworkError; } });
13
+ exports.NOTIFICATION_TYPE_SAM_AUCTION = 'sam_auction';
14
+ function createSubscriptionClient(config) {
15
+ return new client_1.SubscriptionClient(config);
16
+ }
@@ -0,0 +1,6 @@
1
+ /** Builds the signed message string for subscribe operations */
2
+ export declare function subscribeMessage(notificationType: string, channel: string, timestampSeconds: number): string;
3
+ /** Builds the signed message string for unsubscribe operations */
4
+ export declare function unsubscribeMessage(notificationType: string, channel: string, timestampSeconds: number): string;
5
+ /** Builds the signed message string for listing subscriptions */
6
+ export declare function listSubscriptionsMessage(pubkey: string, timestampSeconds: number): string;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.subscribeMessage = subscribeMessage;
4
+ exports.unsubscribeMessage = unsubscribeMessage;
5
+ exports.listSubscriptionsMessage = listSubscriptionsMessage;
6
+ /** Builds the signed message string for subscribe operations */
7
+ function subscribeMessage(notificationType, channel, timestampSeconds) {
8
+ return `Subscribe ${notificationType} ${channel} ${timestampSeconds}`;
9
+ }
10
+ /** Builds the signed message string for unsubscribe operations */
11
+ function unsubscribeMessage(notificationType, channel, timestampSeconds) {
12
+ return `Unsubscribe ${notificationType} ${channel} ${timestampSeconds}`;
13
+ }
14
+ /** Builds the signed message string for listing subscriptions */
15
+ function listSubscriptionsMessage(pubkey, timestampSeconds) {
16
+ return `ListSubscriptions ${pubkey} ${timestampSeconds}`;
17
+ }
@@ -0,0 +1 @@
1
+ {"root":["../client.ts","../index.ts","../message.ts","../types.ts"],"version":"5.8.3"}
@@ -0,0 +1,91 @@
1
+ /** Minimal logger interface — compatible with pino, console, etc. */
2
+ export interface Logger {
3
+ debug(msg: string): void;
4
+ }
5
+ /** Configuration for the subscription client */
6
+ export interface SubscriptionClientConfig {
7
+ base_url: string;
8
+ timeout_ms?: number;
9
+ logger?: Logger;
10
+ }
11
+ /** POST /subscriptions request */
12
+ export interface SubscribeRequest {
13
+ pubkey: string;
14
+ notification_type: string;
15
+ channel: string;
16
+ channel_address: string;
17
+ signature: string;
18
+ message: string;
19
+ additional_data?: Record<string, unknown>;
20
+ }
21
+ /** DELETE /subscriptions request */
22
+ export interface UnsubscribeRequest {
23
+ pubkey: string;
24
+ notification_type: string;
25
+ channel: string;
26
+ channel_address?: string;
27
+ signature: string;
28
+ message: string;
29
+ additional_data?: Record<string, unknown>;
30
+ }
31
+ /** GET /subscriptions auth */
32
+ export interface ListSubscriptionsAuth {
33
+ signature: string;
34
+ message: string;
35
+ }
36
+ /** GET /subscriptions query */
37
+ export interface ListSubscriptionsQuery {
38
+ pubkey: string;
39
+ notification_type?: string;
40
+ additional_data?: Record<string, unknown>;
41
+ }
42
+ /** POST /subscriptions response */
43
+ export interface SubscribeResponse {
44
+ user_id: string;
45
+ notification_type: string;
46
+ channel: string;
47
+ channel_address: string;
48
+ created_at: string;
49
+ deep_link?: string;
50
+ telegram_status?: 'pending' | 'active' | 'inactive' | 'unsubscribed' | 'blocked' | 'already_activated' | 'bot_not_configured';
51
+ }
52
+ /** DELETE /subscriptions response */
53
+ export interface UnsubscribeResponse {
54
+ deleted: boolean;
55
+ }
56
+ /** GET /subscriptions response item */
57
+ export interface Subscription {
58
+ user_id: string;
59
+ notification_type: string;
60
+ channel: string;
61
+ channel_address: string;
62
+ created_at: string;
63
+ telegram_status?: 'pending' | 'active' | 'inactive' | 'unsubscribed' | 'blocked';
64
+ }
65
+ /** GET /notifications query (public, no auth required) */
66
+ export interface ListNotificationsQuery {
67
+ user_id: string;
68
+ notification_type?: string;
69
+ priority?: string;
70
+ inner_type?: string;
71
+ limit?: number;
72
+ offset?: number;
73
+ }
74
+ /** GET /notifications response item */
75
+ export interface Notification {
76
+ id: number;
77
+ notification_type: string;
78
+ inner_type: string;
79
+ user_id: string;
80
+ priority: string;
81
+ message: string;
82
+ data: Record<string, unknown>;
83
+ notification_id: string | null;
84
+ relevance_until: string;
85
+ created_at: string;
86
+ }
87
+ export declare class NetworkError extends Error {
88
+ readonly status?: number | undefined;
89
+ readonly response?: unknown | undefined;
90
+ constructor(message: string, status?: number | undefined, response?: unknown | undefined);
91
+ }
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetworkError = void 0;
4
+ class NetworkError extends Error {
5
+ constructor(message, status, response) {
6
+ super(message);
7
+ this.status = status;
8
+ this.response = response;
9
+ this.name = 'NetworkError';
10
+ }
11
+ }
12
+ exports.NetworkError = NetworkError;
package/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { SubscriptionClient } from './client'
2
+
3
+ import type { SubscriptionClientConfig } from './types'
4
+
5
+ export { SubscriptionClient }
6
+ export {
7
+ subscribeMessage,
8
+ unsubscribeMessage,
9
+ listSubscriptionsMessage,
10
+ } from './message'
11
+ export { NetworkError } from './types'
12
+ export type {
13
+ Logger,
14
+ SubscriptionClientConfig,
15
+ SubscribeRequest,
16
+ SubscribeResponse,
17
+ UnsubscribeRequest,
18
+ UnsubscribeResponse,
19
+ ListSubscriptionsQuery,
20
+ ListSubscriptionsAuth,
21
+ Subscription,
22
+ ListNotificationsQuery,
23
+ Notification,
24
+ } from './types'
25
+
26
+ export const NOTIFICATION_TYPE_SAM_AUCTION = 'sam_auction'
27
+
28
+ export function createSubscriptionClient(
29
+ config: SubscriptionClientConfig,
30
+ ): SubscriptionClient {
31
+ return new SubscriptionClient(config)
32
+ }
package/jest.config.js ADDED
@@ -0,0 +1,10 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+
3
+ module.exports = {
4
+ preset: 'ts-jest',
5
+ rootDir: '.',
6
+ testRegex: '.*\\.test\\.ts$',
7
+ testEnvironment: 'node',
8
+ modulePathIgnorePatterns: ['dist/'],
9
+ testTimeout: 10000,
10
+ }
package/message.ts ADDED
@@ -0,0 +1,25 @@
1
+ /** Builds the signed message string for subscribe operations */
2
+ export function subscribeMessage(
3
+ notificationType: string,
4
+ channel: string,
5
+ timestampSeconds: number,
6
+ ): string {
7
+ return `Subscribe ${notificationType} ${channel} ${timestampSeconds}`
8
+ }
9
+
10
+ /** Builds the signed message string for unsubscribe operations */
11
+ export function unsubscribeMessage(
12
+ notificationType: string,
13
+ channel: string,
14
+ timestampSeconds: number,
15
+ ): string {
16
+ return `Unsubscribe ${notificationType} ${channel} ${timestampSeconds}`
17
+ }
18
+
19
+ /** Builds the signed message string for listing subscriptions */
20
+ export function listSubscriptionsMessage(
21
+ pubkey: string,
22
+ timestampSeconds: number,
23
+ ): string {
24
+ return `ListSubscriptions ${pubkey} ${timestampSeconds}`
25
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@marinade.finance/ts-subscription-client",
3
+ "version": "1.0.0-beta.1",
4
+ "private": false,
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "devDependencies": {
11
+ "@types/jest": "^29.5.14",
12
+ "jest": "^29.7.0",
13
+ "ts-jest": "^29.2.5",
14
+ "typescript": "^5.8.0"
15
+ },
16
+ "engines": {
17
+ "node": ">=20.0.0"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc --build",
21
+ "test": "jest",
22
+ "clean": "rm -rf dist .tsbuildinfo"
23
+ }
24
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "moduleResolution": "node",
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["./**/*.ts"],
16
+ "exclude": [
17
+ "node_modules",
18
+ "dist",
19
+ "**/*.test.ts",
20
+ "**/*.e2e.ts",
21
+ "**/__tests__"
22
+ ]
23
+ }