@solworks/poll-api-client 0.1.0

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,62 @@
1
+ import type { PollClientConfig, Bet, UserAccount, UserProfile, Balance, Wager, LeaderboardEntry, Friend, WalletTransaction, Notification, StreakInfo, ProbabilityPoint, CreateBetParams, PlaceWagerParams, VoteParams, CancelWagerParams, ApiToken } from './types.js';
2
+ export declare class PollClient {
3
+ private apiUrl;
4
+ private token;
5
+ private fetchFn;
6
+ private clientId?;
7
+ constructor(config: PollClientConfig);
8
+ private request;
9
+ private buildUrl;
10
+ getAccount(): Promise<UserAccount>;
11
+ getProfile(uuid?: string): Promise<UserProfile>;
12
+ updateDisplayName(name: string): Promise<void>;
13
+ getXpBalance(): Promise<Balance>;
14
+ getUsdcBalance(): Promise<Balance>;
15
+ listPublicBets(params?: {
16
+ page?: number;
17
+ }): Promise<Bet[]>;
18
+ getTrendingBets(): Promise<Bet[]>;
19
+ getNewBets(): Promise<Bet[]>;
20
+ getBet(id: string): Promise<Bet>;
21
+ getMyBets(): Promise<Bet[]>;
22
+ createBet(params: CreateBetParams): Promise<Bet>;
23
+ joinBet(betAddress: string): Promise<unknown>;
24
+ placeWager(params: PlaceWagerParams): Promise<unknown>;
25
+ cancelWager(params: CancelWagerParams): Promise<unknown>;
26
+ initiateVote(betAddress: string): Promise<unknown>;
27
+ vote(params: VoteParams): Promise<unknown>;
28
+ settleBet(betAddress: string): Promise<unknown>;
29
+ getProbabilityHistory(betAddress: string): Promise<ProbabilityPoint[]>;
30
+ getMyWagers(params?: {
31
+ active?: boolean;
32
+ }): Promise<Wager[]>;
33
+ getLeaderboard(): Promise<LeaderboardEntry[]>;
34
+ getMyRanking(): Promise<LeaderboardEntry>;
35
+ getFriends(): Promise<Friend[]>;
36
+ sendFriendRequest(uuid: string): Promise<unknown>;
37
+ respondFriendRequest(id: string, accept: boolean): Promise<unknown>;
38
+ getFavouriteBets(): Promise<Bet[]>;
39
+ favouriteBet(betAddress: string): Promise<unknown>;
40
+ unfavouriteBet(betAddress: string): Promise<unknown>;
41
+ nudgeUser(betPubkey: string, recipientUuid: string): Promise<unknown>;
42
+ getWalletTransactions(): Promise<WalletTransaction[]>;
43
+ getNotifications(params?: {
44
+ page?: number;
45
+ }): Promise<Notification[]>;
46
+ markNotificationRead(id: string): Promise<unknown>;
47
+ getStreak(): Promise<StreakInfo>;
48
+ getTokens(): Promise<{
49
+ tokens: ApiToken[];
50
+ }>;
51
+ createToken(params: {
52
+ name: string;
53
+ scopes: string[];
54
+ expiresAt?: string;
55
+ }): Promise<{
56
+ token: string;
57
+ id: string;
58
+ }>;
59
+ revokeToken(tokenId: string): Promise<{
60
+ success: boolean;
61
+ }>;
62
+ }
package/dist/client.js ADDED
@@ -0,0 +1,200 @@
1
+ import { PollApiError } from './errors.js';
2
+ export class PollClient {
3
+ apiUrl;
4
+ token;
5
+ fetchFn;
6
+ clientId;
7
+ constructor(config) {
8
+ const url = config.apiUrl.replace(/\/+$/, '');
9
+ if (!url.startsWith('https://') && !url.startsWith('http://localhost') && !url.startsWith('http://127.0.0.1')) {
10
+ throw new Error('API URL must use HTTPS (except localhost for development)');
11
+ }
12
+ this.apiUrl = url;
13
+ this.token = config.token;
14
+ this.fetchFn = config.fetch ?? globalThis.fetch;
15
+ this.clientId = config.clientId;
16
+ }
17
+ async request(method, path, body) {
18
+ const url = `${this.apiUrl}${path}`;
19
+ const headers = {
20
+ 'Authorization': `Bearer ${this.token}`,
21
+ 'Content-Type': 'application/json',
22
+ };
23
+ if (this.clientId) {
24
+ headers['X-Client'] = this.clientId;
25
+ }
26
+ let response;
27
+ try {
28
+ response = await this.fetchFn(url, {
29
+ method,
30
+ headers,
31
+ body: body !== undefined ? JSON.stringify(body) : undefined,
32
+ });
33
+ }
34
+ catch (err) {
35
+ throw new PollApiError(0, `Network error: ${err.message}`);
36
+ }
37
+ if (!response.ok) {
38
+ let errorBody;
39
+ let errorMessage;
40
+ try {
41
+ errorBody = await response.json();
42
+ errorMessage =
43
+ errorBody?.error ??
44
+ errorBody?.message ??
45
+ response.statusText;
46
+ }
47
+ catch {
48
+ try {
49
+ const text = await response.text();
50
+ errorMessage = text || response.statusText;
51
+ errorBody = text;
52
+ }
53
+ catch {
54
+ errorMessage = response.statusText;
55
+ }
56
+ }
57
+ throw new PollApiError(response.status, errorMessage, errorBody);
58
+ }
59
+ return response.json();
60
+ }
61
+ buildUrl(path, params) {
62
+ if (!params)
63
+ return path;
64
+ const searchParams = new URLSearchParams();
65
+ for (const [key, value] of Object.entries(params)) {
66
+ if (value !== undefined) {
67
+ searchParams.set(key, String(value));
68
+ }
69
+ }
70
+ const qs = searchParams.toString();
71
+ return qs ? `${path}?${qs}` : path;
72
+ }
73
+ // ── User ──────────────────────────────────────────────────────────────
74
+ async getAccount() {
75
+ return this.request('GET', '/api/user/account');
76
+ }
77
+ async getProfile(uuid) {
78
+ const path = uuid ? `/api/user/profile/${uuid}` : '/api/user/profile';
79
+ return this.request('GET', path);
80
+ }
81
+ async updateDisplayName(name) {
82
+ await this.request('POST', '/api/user/account/displayname', {
83
+ displayName: name,
84
+ });
85
+ }
86
+ async getXpBalance() {
87
+ return this.request('GET', '/api/user/balance');
88
+ }
89
+ async getUsdcBalance() {
90
+ return this.request('GET', '/api/user/usdc/balance');
91
+ }
92
+ // ── Bets ──────────────────────────────────────────────────────────────
93
+ async listPublicBets(params) {
94
+ const path = this.buildUrl('/api/bets-usdc/all/public', params);
95
+ return this.request('GET', path);
96
+ }
97
+ async getTrendingBets() {
98
+ return this.request('GET', '/api/bets-usdc/all/public/trending');
99
+ }
100
+ async getNewBets() {
101
+ return this.request('GET', '/api/bets-usdc/all/public/new');
102
+ }
103
+ async getBet(id) {
104
+ return this.request('GET', `/api/bets-usdc/all/public/single/${id}`);
105
+ }
106
+ async getMyBets() {
107
+ return this.request('GET', '/api/bets-usdc/mine');
108
+ }
109
+ async createBet(params) {
110
+ return this.request('POST', '/api/bets-usdc', params);
111
+ }
112
+ async joinBet(betAddress) {
113
+ return this.request('POST', '/api/bets-usdc/join', { betAddress });
114
+ }
115
+ async placeWager(params) {
116
+ return this.request('POST', '/api/bets-usdc/wager', params);
117
+ }
118
+ async cancelWager(params) {
119
+ return this.request('POST', '/api/bets-usdc/wager/cancel', params);
120
+ }
121
+ async initiateVote(betAddress) {
122
+ return this.request('POST', '/api/bets-usdc/vote/initiate', {
123
+ betAddress,
124
+ });
125
+ }
126
+ async vote(params) {
127
+ return this.request('POST', '/api/bets-usdc/vote', params);
128
+ }
129
+ async settleBet(betAddress) {
130
+ return this.request('POST', '/api/bets-usdc/settle', { betAddress });
131
+ }
132
+ async getProbabilityHistory(betAddress) {
133
+ return this.request('GET', `/api/bets-usdc/all/public/single/${betAddress}/probability-history`);
134
+ }
135
+ // ── Wagers ────────────────────────────────────────────────────────────
136
+ async getMyWagers(params) {
137
+ const path = this.buildUrl('/api/bets-usdc/wagers', params);
138
+ return this.request('GET', path);
139
+ }
140
+ // ── Leaderboard ───────────────────────────────────────────────────────
141
+ async getLeaderboard() {
142
+ return this.request('GET', '/api/leaderboard');
143
+ }
144
+ async getMyRanking() {
145
+ return this.request('GET', '/api/leaderboard/me');
146
+ }
147
+ // ── Social ────────────────────────────────────────────────────────────
148
+ async getFriends() {
149
+ return this.request('GET', '/api/user/friends');
150
+ }
151
+ async sendFriendRequest(uuid) {
152
+ return this.request('POST', '/api/user/friends/request', { uuid });
153
+ }
154
+ async respondFriendRequest(id, accept) {
155
+ const action = accept ? 'accept' : 'decline';
156
+ return this.request('PUT', `/api/user/friends/request/${id}/${action}`);
157
+ }
158
+ async getFavouriteBets() {
159
+ return this.request('GET', '/api/user/favourites/bets');
160
+ }
161
+ async favouriteBet(betAddress) {
162
+ return this.request('POST', '/api/user/favourites/bets', {
163
+ betAddress,
164
+ });
165
+ }
166
+ async unfavouriteBet(betAddress) {
167
+ return this.request('DELETE', `/api/user/favourites/bets/${betAddress}`);
168
+ }
169
+ async nudgeUser(betPubkey, recipientUuid) {
170
+ return this.request('POST', `/api/nudge/${betPubkey}/${recipientUuid}`);
171
+ }
172
+ // ── Wallet ────────────────────────────────────────────────────────────
173
+ async getWalletTransactions() {
174
+ return this.request('GET', '/api/user/wallet/transactions');
175
+ }
176
+ // ── Notifications ─────────────────────────────────────────────────────
177
+ async getNotifications(params) {
178
+ const path = this.buildUrl('/api/user/notifications', params);
179
+ return this.request('GET', path);
180
+ }
181
+ async markNotificationRead(id) {
182
+ return this.request('POST', '/api/user/notification/read', {
183
+ notificationId: id,
184
+ });
185
+ }
186
+ // ── Meta ──────────────────────────────────────────────────────────────
187
+ async getStreak() {
188
+ return this.request('GET', '/api/user/streak');
189
+ }
190
+ // ── Tokens ────────────────────────────────────────────────────────────
191
+ async getTokens() {
192
+ return this.request('GET', '/api/auth/tokens');
193
+ }
194
+ async createToken(params) {
195
+ return this.request('POST', '/api/auth/token', params);
196
+ }
197
+ async revokeToken(tokenId) {
198
+ return this.request('DELETE', `/api/auth/token/${tokenId}`);
199
+ }
200
+ }
@@ -0,0 +1,6 @@
1
+ export declare class PollApiError extends Error {
2
+ statusCode: number;
3
+ errorMessage: string;
4
+ rawResponse?: unknown | undefined;
5
+ constructor(statusCode: number, errorMessage: string, rawResponse?: unknown | undefined);
6
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,12 @@
1
+ export class PollApiError extends Error {
2
+ statusCode;
3
+ errorMessage;
4
+ rawResponse;
5
+ constructor(statusCode, errorMessage, rawResponse) {
6
+ super(`API Error ${statusCode}: ${errorMessage}`);
7
+ this.statusCode = statusCode;
8
+ this.errorMessage = errorMessage;
9
+ this.rawResponse = rawResponse;
10
+ this.name = 'PollApiError';
11
+ }
12
+ }
@@ -0,0 +1,3 @@
1
+ export { PollClient } from './client.js';
2
+ export { PollApiError } from './errors.js';
3
+ export type { PollClientConfig, Bet, UserAccount, UserProfile, Balance, Wager, LeaderboardEntry, Friend, WalletTransaction, Notification, StreakInfo, ProbabilityPoint, CreateBetParams, PlaceWagerParams, VoteParams, CancelWagerParams, ApiToken, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { PollClient } from './client.js';
2
+ export { PollApiError } from './errors.js';
@@ -0,0 +1,118 @@
1
+ export interface PollClientConfig {
2
+ apiUrl: string;
3
+ token: string;
4
+ fetch?: typeof globalThis.fetch;
5
+ clientId?: string;
6
+ }
7
+ export interface Bet {
8
+ id: string;
9
+ question: string;
10
+ options: string[];
11
+ status: string;
12
+ totalVolume: number;
13
+ createdAt: string;
14
+ betAddress?: string;
15
+ inviteCode?: string;
16
+ [key: string]: unknown;
17
+ }
18
+ export interface UserAccount {
19
+ id: number;
20
+ uuid: string;
21
+ displayName: string | null;
22
+ email: string | null;
23
+ xpBalance: number;
24
+ balance: number;
25
+ [key: string]: unknown;
26
+ }
27
+ export interface UserProfile {
28
+ id: number;
29
+ uuid: string;
30
+ displayName: string | null;
31
+ [key: string]: unknown;
32
+ }
33
+ export interface Balance {
34
+ xp: number;
35
+ usdc?: number;
36
+ error: string | null;
37
+ }
38
+ export interface Wager {
39
+ id: string;
40
+ betAddress: string;
41
+ amount: number;
42
+ optionIndex: number;
43
+ status: string;
44
+ [key: string]: unknown;
45
+ }
46
+ export interface LeaderboardEntry {
47
+ userId: number;
48
+ uuid: string;
49
+ displayName: string | null;
50
+ rank: number;
51
+ points: number;
52
+ [key: string]: unknown;
53
+ }
54
+ export interface Friend {
55
+ id: number;
56
+ uuid: string;
57
+ displayName: string | null;
58
+ [key: string]: unknown;
59
+ }
60
+ export interface WalletTransaction {
61
+ signature: string;
62
+ type: string;
63
+ amount: number;
64
+ createdAt: string;
65
+ [key: string]: unknown;
66
+ }
67
+ export interface Notification {
68
+ id: string;
69
+ type: string;
70
+ title: string;
71
+ body?: string;
72
+ read: boolean;
73
+ createdAt: string;
74
+ [key: string]: unknown;
75
+ }
76
+ export interface StreakInfo {
77
+ currentStreak: number;
78
+ longestStreak: number;
79
+ [key: string]: unknown;
80
+ }
81
+ export interface ProbabilityPoint {
82
+ timestamp: string;
83
+ probability: number;
84
+ [key: string]: unknown;
85
+ }
86
+ export interface CreateBetParams {
87
+ question: string;
88
+ options: string[];
89
+ type: string;
90
+ amount?: number;
91
+ expiresAt?: string;
92
+ [key: string]: unknown;
93
+ }
94
+ export interface PlaceWagerParams {
95
+ betAddress: string;
96
+ optionIndex: number;
97
+ amount: number;
98
+ [key: string]: unknown;
99
+ }
100
+ export interface VoteParams {
101
+ betAddress: string;
102
+ optionIndex: number;
103
+ [key: string]: unknown;
104
+ }
105
+ export interface CancelWagerParams {
106
+ betAddress: string;
107
+ wagerAddress: string;
108
+ [key: string]: unknown;
109
+ }
110
+ export interface ApiToken {
111
+ id: string;
112
+ name: string;
113
+ tokenPrefix: string;
114
+ scopes: string[];
115
+ createdAt: string;
116
+ lastUsedAt: string | null;
117
+ expiresAt: string | null;
118
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@solworks/poll-api-client",
3
+ "version": "0.1.0",
4
+ "description": "Typed HTTP client for the Poll.fun REST API",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "vitest run",
10
+ "test:watch": "vitest",
11
+ "test:coverage": "vitest run --coverage",
12
+ "test:unit": "vitest run tests/unit"
13
+ },
14
+ "devDependencies": {
15
+ "typescript": "^5.4.0",
16
+ "vitest": "^3.0.0"
17
+ }
18
+ }
@@ -0,0 +1,54 @@
1
+ import type { Bet, UserAccount, Wager } from '../../src/types';
2
+
3
+ let counter = 0;
4
+ function nextId(): string {
5
+ counter++;
6
+ return `fixture-${counter}-${Date.now()}`;
7
+ }
8
+
9
+ export function createBetFixture(overrides?: Partial<Bet>): Bet {
10
+ return {
11
+ id: nextId(),
12
+ question: 'Will BTC hit $100k by end of year?',
13
+ options: ['Yes', 'No'],
14
+ status: 'OPEN',
15
+ totalVolume: 5000,
16
+ createdAt: '2026-01-15T12:00:00.000Z',
17
+ betAddress: `bet_${nextId()}`,
18
+ inviteCode: 'ABC123',
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ export function createUserFixture(overrides?: Partial<UserAccount>): UserAccount {
24
+ const id = counter++;
25
+ return {
26
+ id,
27
+ uuid: `user-uuid-${id}`,
28
+ displayName: `TestUser${id}`,
29
+ email: `user${id}@test.com`,
30
+ xpBalance: 1000,
31
+ balance: 250,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ export function createWagerFixture(overrides?: Partial<Wager>): Wager {
37
+ return {
38
+ id: nextId(),
39
+ betAddress: `bet_${nextId()}`,
40
+ amount: 100,
41
+ optionIndex: 0,
42
+ status: 'PLACED',
43
+ ...overrides,
44
+ };
45
+ }
46
+
47
+ export function createTokenFixture(): string {
48
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
49
+ let suffix = '';
50
+ for (let i = 0; i < 32; i++) {
51
+ suffix += chars[Math.floor(Math.random() * chars.length)];
52
+ }
53
+ return `polld_${suffix}`;
54
+ }
@@ -0,0 +1,45 @@
1
+ import { vi } from 'vitest';
2
+
3
+ export function createMockFetch(): ReturnType<typeof vi.fn> & typeof globalThis.fetch {
4
+ return vi.fn() as ReturnType<typeof vi.fn> & typeof globalThis.fetch;
5
+ }
6
+
7
+ export function mockJsonResponse(data: unknown, status = 200): Response {
8
+ return {
9
+ ok: status >= 200 && status < 300,
10
+ status,
11
+ statusText: statusTextForCode(status),
12
+ json: () => Promise.resolve(data),
13
+ text: () => Promise.resolve(JSON.stringify(data)),
14
+ headers: new Headers({ 'content-type': 'application/json' }),
15
+ redirected: false,
16
+ type: 'basic' as ResponseType,
17
+ url: '',
18
+ clone: () => mockJsonResponse(data, status),
19
+ body: null,
20
+ bodyUsed: false,
21
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
22
+ blob: () => Promise.resolve(new Blob()),
23
+ formData: () => Promise.resolve(new FormData()),
24
+ bytes: () => Promise.resolve(new Uint8Array()),
25
+ } as Response;
26
+ }
27
+
28
+ export function mockErrorResponse(error: string, status: number): Response {
29
+ return mockJsonResponse({ error }, status);
30
+ }
31
+
32
+ function statusTextForCode(status: number): string {
33
+ const map: Record<number, string> = {
34
+ 200: 'OK',
35
+ 201: 'Created',
36
+ 204: 'No Content',
37
+ 400: 'Bad Request',
38
+ 401: 'Unauthorized',
39
+ 403: 'Forbidden',
40
+ 404: 'Not Found',
41
+ 500: 'Internal Server Error',
42
+ 502: 'Bad Gateway',
43
+ };
44
+ return map[status] ?? 'Unknown';
45
+ }
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PollClient } from '../../src/client';
3
+ import { PollApiError } from '../../src/errors';
4
+ import { createMockFetch, mockJsonResponse, mockErrorResponse } from '../helpers/mock-fetch';
5
+ import { createUserFixture } from '../fixtures';
6
+
7
+ describe('PollClient core request logic', () => {
8
+ let mockFetch: ReturnType<typeof createMockFetch>;
9
+
10
+ beforeEach(() => {
11
+ mockFetch = createMockFetch();
12
+ });
13
+
14
+ it('sets Authorization header correctly', async () => {
15
+ const user = createUserFixture();
16
+ mockFetch.mockResolvedValue(mockJsonResponse(user));
17
+
18
+ const client = new PollClient({
19
+ apiUrl: 'https://api.poll.fun',
20
+ token: 'my-secret-token',
21
+ fetch: mockFetch,
22
+ });
23
+
24
+ await client.getAccount();
25
+
26
+ expect(mockFetch).toHaveBeenCalledTimes(1);
27
+ const [url, init] = mockFetch.mock.calls[0];
28
+ expect(url).toBe('https://api.poll.fun/api/user/account');
29
+ expect(init.headers['Authorization']).toBe('Bearer my-secret-token');
30
+ expect(init.headers['Content-Type']).toBe('application/json');
31
+ });
32
+
33
+ it('sets X-Client header if clientId provided', async () => {
34
+ const user = createUserFixture();
35
+ mockFetch.mockResolvedValue(mockJsonResponse(user));
36
+
37
+ const client = new PollClient({
38
+ apiUrl: 'https://api.poll.fun',
39
+ token: 'tok',
40
+ fetch: mockFetch,
41
+ clientId: 'poll-mcp',
42
+ });
43
+
44
+ await client.getAccount();
45
+
46
+ const [, init] = mockFetch.mock.calls[0];
47
+ expect(init.headers['X-Client']).toBe('poll-mcp');
48
+ });
49
+
50
+ it('does not set X-Client header if clientId not provided', async () => {
51
+ const user = createUserFixture();
52
+ mockFetch.mockResolvedValue(mockJsonResponse(user));
53
+
54
+ const client = new PollClient({
55
+ apiUrl: 'https://api.poll.fun',
56
+ token: 'tok',
57
+ fetch: mockFetch,
58
+ });
59
+
60
+ await client.getAccount();
61
+
62
+ const [, init] = mockFetch.mock.calls[0];
63
+ expect(init.headers['X-Client']).toBeUndefined();
64
+ });
65
+
66
+ it('handles 401 errors', async () => {
67
+ mockFetch.mockResolvedValue(mockErrorResponse('Unauthorized', 401));
68
+
69
+ const client = new PollClient({
70
+ apiUrl: 'https://api.poll.fun',
71
+ token: 'bad-token',
72
+ fetch: mockFetch,
73
+ });
74
+
75
+ await expect(client.getAccount()).rejects.toThrow(PollApiError);
76
+ await expect(client.getAccount()).rejects.toMatchObject({
77
+ statusCode: 401,
78
+ errorMessage: 'Unauthorized',
79
+ });
80
+ });
81
+
82
+ it('handles 403 scope errors', async () => {
83
+ mockFetch.mockResolvedValue(
84
+ mockErrorResponse('Insufficient scope: requires bets:read', 403)
85
+ );
86
+
87
+ const client = new PollClient({
88
+ apiUrl: 'https://api.poll.fun',
89
+ token: 'tok',
90
+ fetch: mockFetch,
91
+ });
92
+
93
+ await expect(client.getAccount()).rejects.toThrow(PollApiError);
94
+ await expect(client.getAccount()).rejects.toMatchObject({
95
+ statusCode: 403,
96
+ errorMessage: 'Insufficient scope: requires bets:read',
97
+ });
98
+ });
99
+
100
+ it('handles 502 non-JSON errors', async () => {
101
+ const response502: Response = {
102
+ ok: false,
103
+ status: 502,
104
+ statusText: 'Bad Gateway',
105
+ json: () => Promise.reject(new Error('not JSON')),
106
+ text: () => Promise.resolve('<html>Bad Gateway</html>'),
107
+ headers: new Headers(),
108
+ redirected: false,
109
+ type: 'basic' as ResponseType,
110
+ url: '',
111
+ clone: () => response502,
112
+ body: null,
113
+ bodyUsed: false,
114
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
115
+ blob: () => Promise.resolve(new Blob()),
116
+ formData: () => Promise.resolve(new FormData()),
117
+ bytes: () => Promise.resolve(new Uint8Array()),
118
+ } as Response;
119
+
120
+ mockFetch.mockResolvedValue(response502);
121
+
122
+ const client = new PollClient({
123
+ apiUrl: 'https://api.poll.fun',
124
+ token: 'tok',
125
+ fetch: mockFetch,
126
+ });
127
+
128
+ await expect(client.getAccount()).rejects.toThrow(PollApiError);
129
+ await expect(client.getAccount()).rejects.toMatchObject({
130
+ statusCode: 502,
131
+ errorMessage: '<html>Bad Gateway</html>',
132
+ });
133
+ });
134
+
135
+ it('handles network errors (fetch throws)', async () => {
136
+ mockFetch.mockRejectedValue(new Error('ECONNREFUSED'));
137
+
138
+ const client = new PollClient({
139
+ apiUrl: 'https://api.poll.fun',
140
+ token: 'tok',
141
+ fetch: mockFetch,
142
+ });
143
+
144
+ await expect(client.getAccount()).rejects.toThrow(PollApiError);
145
+ await expect(client.getAccount()).rejects.toMatchObject({
146
+ statusCode: 0,
147
+ errorMessage: 'Network error: ECONNREFUSED',
148
+ });
149
+ });
150
+
151
+ it('strips trailing slashes from apiUrl', async () => {
152
+ const user = createUserFixture();
153
+ mockFetch.mockResolvedValue(mockJsonResponse(user));
154
+
155
+ const client = new PollClient({
156
+ apiUrl: 'https://api.poll.fun///',
157
+ token: 'tok',
158
+ fetch: mockFetch,
159
+ });
160
+
161
+ await client.getAccount();
162
+
163
+ const [url] = mockFetch.mock.calls[0];
164
+ expect(url).toBe('https://api.poll.fun/api/user/account');
165
+ });
166
+ });
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PollClient } from '../../../src/client';
3
+ import { createMockFetch, mockJsonResponse } from '../../helpers/mock-fetch';
4
+ import { createBetFixture } from '../../fixtures';
5
+
6
+ describe('Bets endpoints', () => {
7
+ let mockFetch: ReturnType<typeof createMockFetch>;
8
+ let client: PollClient;
9
+
10
+ beforeEach(() => {
11
+ mockFetch = createMockFetch();
12
+ client = new PollClient({
13
+ apiUrl: 'https://api.poll.fun',
14
+ token: 'test-token',
15
+ fetch: mockFetch,
16
+ });
17
+ });
18
+
19
+ it('listPublicBets sends correct GET', async () => {
20
+ const bets = [createBetFixture(), createBetFixture()];
21
+ mockFetch.mockResolvedValue(mockJsonResponse(bets));
22
+
23
+ const result = await client.listPublicBets();
24
+
25
+ expect(result).toEqual(bets);
26
+ const [url, init] = mockFetch.mock.calls[0];
27
+ expect(url).toBe('https://api.poll.fun/api/bets-usdc/all/public');
28
+ expect(init.method).toBe('GET');
29
+ });
30
+
31
+ it('listPublicBets with page param', async () => {
32
+ const bets = [createBetFixture()];
33
+ mockFetch.mockResolvedValue(mockJsonResponse(bets));
34
+
35
+ await client.listPublicBets({ page: 2 });
36
+
37
+ const [url] = mockFetch.mock.calls[0];
38
+ expect(url).toBe('https://api.poll.fun/api/bets-usdc/all/public?page=2');
39
+ });
40
+
41
+ it('getTrendingBets sends correct GET', async () => {
42
+ const bets = [createBetFixture()];
43
+ mockFetch.mockResolvedValue(mockJsonResponse(bets));
44
+
45
+ const result = await client.getTrendingBets();
46
+
47
+ expect(result).toEqual(bets);
48
+ const [url, init] = mockFetch.mock.calls[0];
49
+ expect(url).toBe('https://api.poll.fun/api/bets-usdc/all/public/trending');
50
+ expect(init.method).toBe('GET');
51
+ });
52
+
53
+ it('getBet sends GET with ID', async () => {
54
+ const bet = createBetFixture({ id: 'bet-123' });
55
+ mockFetch.mockResolvedValue(mockJsonResponse(bet));
56
+
57
+ const result = await client.getBet('bet-123');
58
+
59
+ expect(result).toEqual(bet);
60
+ const [url, init] = mockFetch.mock.calls[0];
61
+ expect(url).toBe('https://api.poll.fun/api/bets-usdc/all/public/single/bet-123');
62
+ expect(init.method).toBe('GET');
63
+ });
64
+
65
+ it('createBet sends POST with body', async () => {
66
+ const newBet = createBetFixture();
67
+ mockFetch.mockResolvedValue(mockJsonResponse(newBet));
68
+
69
+ const params = {
70
+ question: 'Will it rain tomorrow?',
71
+ options: ['Yes', 'No'],
72
+ type: 'XP',
73
+ amount: 100,
74
+ };
75
+
76
+ const result = await client.createBet(params);
77
+
78
+ expect(result).toEqual(newBet);
79
+ const [url, init] = mockFetch.mock.calls[0];
80
+ expect(url).toBe('https://api.poll.fun/api/bets-usdc');
81
+ expect(init.method).toBe('POST');
82
+ expect(JSON.parse(init.body)).toEqual(params);
83
+ });
84
+
85
+ it('placeWager sends POST with body', async () => {
86
+ mockFetch.mockResolvedValue(mockJsonResponse({ success: true }));
87
+
88
+ const params = {
89
+ betAddress: 'bet_abc',
90
+ optionIndex: 1,
91
+ amount: 50,
92
+ };
93
+
94
+ await client.placeWager(params);
95
+
96
+ const [url, init] = mockFetch.mock.calls[0];
97
+ expect(url).toBe('https://api.poll.fun/api/bets-usdc/wager');
98
+ expect(init.method).toBe('POST');
99
+ expect(JSON.parse(init.body)).toEqual(params);
100
+ });
101
+
102
+ it('vote sends POST with body', async () => {
103
+ mockFetch.mockResolvedValue(mockJsonResponse({ success: true }));
104
+
105
+ const params = {
106
+ betAddress: 'bet_abc',
107
+ optionIndex: 0,
108
+ };
109
+
110
+ await client.vote(params);
111
+
112
+ const [url, init] = mockFetch.mock.calls[0];
113
+ expect(url).toBe('https://api.poll.fun/api/bets-usdc/vote');
114
+ expect(init.method).toBe('POST');
115
+ expect(JSON.parse(init.body)).toEqual(params);
116
+ });
117
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PollClient } from '../../../src/client';
3
+ import { createMockFetch, mockJsonResponse } from '../../helpers/mock-fetch';
4
+
5
+ describe('Leaderboard endpoints', () => {
6
+ let mockFetch: ReturnType<typeof createMockFetch>;
7
+ let client: PollClient;
8
+
9
+ beforeEach(() => {
10
+ mockFetch = createMockFetch();
11
+ client = new PollClient({
12
+ apiUrl: 'https://api.poll.fun',
13
+ token: 'test-token',
14
+ fetch: mockFetch,
15
+ });
16
+ });
17
+
18
+ it('getLeaderboard sends correct GET', async () => {
19
+ const entries = [
20
+ { userId: 1, uuid: 'u1', displayName: 'User1', rank: 1, points: 1000 },
21
+ { userId: 2, uuid: 'u2', displayName: 'User2', rank: 2, points: 800 },
22
+ ];
23
+ mockFetch.mockResolvedValue(mockJsonResponse(entries));
24
+
25
+ const result = await client.getLeaderboard();
26
+
27
+ expect(result).toEqual(entries);
28
+ const [url, init] = mockFetch.mock.calls[0];
29
+ expect(url).toBe('https://api.poll.fun/api/leaderboard');
30
+ expect(init.method).toBe('GET');
31
+ });
32
+
33
+ it('getMyRanking sends correct GET', async () => {
34
+ const entry = { userId: 1, uuid: 'u1', displayName: 'Me', rank: 5, points: 500 };
35
+ mockFetch.mockResolvedValue(mockJsonResponse(entry));
36
+
37
+ const result = await client.getMyRanking();
38
+
39
+ expect(result).toEqual(entry);
40
+ const [url, init] = mockFetch.mock.calls[0];
41
+ expect(url).toBe('https://api.poll.fun/api/leaderboard/me');
42
+ expect(init.method).toBe('GET');
43
+ });
44
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PollClient } from '../../../src/client';
3
+ import { createMockFetch, mockJsonResponse } from '../../helpers/mock-fetch';
4
+
5
+ describe('Social endpoints', () => {
6
+ let mockFetch: ReturnType<typeof createMockFetch>;
7
+ let client: PollClient;
8
+
9
+ beforeEach(() => {
10
+ mockFetch = createMockFetch();
11
+ client = new PollClient({
12
+ apiUrl: 'https://api.poll.fun',
13
+ token: 'test-token',
14
+ fetch: mockFetch,
15
+ });
16
+ });
17
+
18
+ it('getFriends sends correct GET', async () => {
19
+ const friends = [
20
+ { id: 1, uuid: 'f1', displayName: 'Friend1' },
21
+ { id: 2, uuid: 'f2', displayName: 'Friend2' },
22
+ ];
23
+ mockFetch.mockResolvedValue(mockJsonResponse(friends));
24
+
25
+ const result = await client.getFriends();
26
+
27
+ expect(result).toEqual(friends);
28
+ const [url, init] = mockFetch.mock.calls[0];
29
+ expect(url).toBe('https://api.poll.fun/api/user/friends');
30
+ expect(init.method).toBe('GET');
31
+ });
32
+
33
+ it('sendFriendRequest sends POST', async () => {
34
+ mockFetch.mockResolvedValue(mockJsonResponse({ success: true }));
35
+
36
+ await client.sendFriendRequest('target-uuid');
37
+
38
+ const [url, init] = mockFetch.mock.calls[0];
39
+ expect(url).toBe('https://api.poll.fun/api/user/friends/request');
40
+ expect(init.method).toBe('POST');
41
+ expect(JSON.parse(init.body)).toEqual({ uuid: 'target-uuid' });
42
+ });
43
+
44
+ it('favouriteBet sends POST', async () => {
45
+ mockFetch.mockResolvedValue(mockJsonResponse({ success: true }));
46
+
47
+ await client.favouriteBet('bet_xyz');
48
+
49
+ const [url, init] = mockFetch.mock.calls[0];
50
+ expect(url).toBe('https://api.poll.fun/api/user/favourites/bets');
51
+ expect(init.method).toBe('POST');
52
+ expect(JSON.parse(init.body)).toEqual({ betAddress: 'bet_xyz' });
53
+ });
54
+
55
+ it('unfavouriteBet sends DELETE', async () => {
56
+ mockFetch.mockResolvedValue(mockJsonResponse({ success: true }));
57
+
58
+ await client.unfavouriteBet('bet_xyz');
59
+
60
+ const [url, init] = mockFetch.mock.calls[0];
61
+ expect(url).toBe('https://api.poll.fun/api/user/favourites/bets/bet_xyz');
62
+ expect(init.method).toBe('DELETE');
63
+ });
64
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PollClient } from '../../../src/client';
3
+ import { createMockFetch, mockJsonResponse } from '../../helpers/mock-fetch';
4
+ import { createUserFixture } from '../../fixtures';
5
+
6
+ describe('User endpoints', () => {
7
+ let mockFetch: ReturnType<typeof createMockFetch>;
8
+ let client: PollClient;
9
+
10
+ beforeEach(() => {
11
+ mockFetch = createMockFetch();
12
+ client = new PollClient({
13
+ apiUrl: 'https://api.poll.fun',
14
+ token: 'test-token',
15
+ fetch: mockFetch,
16
+ });
17
+ });
18
+
19
+ it('getAccount sends correct GET', async () => {
20
+ const user = createUserFixture();
21
+ mockFetch.mockResolvedValue(mockJsonResponse(user));
22
+
23
+ const result = await client.getAccount();
24
+
25
+ expect(result).toEqual(user);
26
+ const [url, init] = mockFetch.mock.calls[0];
27
+ expect(url).toBe('https://api.poll.fun/api/user/account');
28
+ expect(init.method).toBe('GET');
29
+ });
30
+
31
+ it('getProfile without uuid', async () => {
32
+ const profile = { id: 1, uuid: 'abc', displayName: 'Test' };
33
+ mockFetch.mockResolvedValue(mockJsonResponse(profile));
34
+
35
+ const result = await client.getProfile();
36
+
37
+ expect(result).toEqual(profile);
38
+ const [url] = mockFetch.mock.calls[0];
39
+ expect(url).toBe('https://api.poll.fun/api/user/profile');
40
+ });
41
+
42
+ it('getProfile with uuid sends GET /api/user/profile/:uuid', async () => {
43
+ const profile = { id: 2, uuid: 'user-uuid-123', displayName: 'OtherUser' };
44
+ mockFetch.mockResolvedValue(mockJsonResponse(profile));
45
+
46
+ const result = await client.getProfile('user-uuid-123');
47
+
48
+ expect(result).toEqual(profile);
49
+ const [url] = mockFetch.mock.calls[0];
50
+ expect(url).toBe('https://api.poll.fun/api/user/profile/user-uuid-123');
51
+ });
52
+
53
+ it('updateDisplayName sends POST', async () => {
54
+ mockFetch.mockResolvedValue(mockJsonResponse({}));
55
+
56
+ await client.updateDisplayName('NewName');
57
+
58
+ const [url, init] = mockFetch.mock.calls[0];
59
+ expect(url).toBe('https://api.poll.fun/api/user/account/displayname');
60
+ expect(init.method).toBe('POST');
61
+ expect(JSON.parse(init.body)).toEqual({ displayName: 'NewName' });
62
+ });
63
+
64
+ it('getXpBalance sends correct GET', async () => {
65
+ const balance = { xp: 500, error: null };
66
+ mockFetch.mockResolvedValue(mockJsonResponse(balance));
67
+
68
+ const result = await client.getXpBalance();
69
+
70
+ expect(result).toEqual(balance);
71
+ const [url, init] = mockFetch.mock.calls[0];
72
+ expect(url).toBe('https://api.poll.fun/api/user/balance');
73
+ expect(init.method).toBe('GET');
74
+ });
75
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PollClient } from '../../../src/client';
3
+ import { createMockFetch, mockJsonResponse } from '../../helpers/mock-fetch';
4
+
5
+ describe('Wallet endpoints', () => {
6
+ let mockFetch: ReturnType<typeof createMockFetch>;
7
+ let client: PollClient;
8
+
9
+ beforeEach(() => {
10
+ mockFetch = createMockFetch();
11
+ client = new PollClient({
12
+ apiUrl: 'https://api.poll.fun',
13
+ token: 'test-token',
14
+ fetch: mockFetch,
15
+ });
16
+ });
17
+
18
+ it('getWalletTransactions sends correct GET', async () => {
19
+ const txns = [
20
+ {
21
+ signature: 'sig123',
22
+ type: 'DEPOSIT',
23
+ amount: 100,
24
+ createdAt: '2026-01-01T00:00:00.000Z',
25
+ },
26
+ {
27
+ signature: 'sig456',
28
+ type: 'WITHDRAWAL',
29
+ amount: 50,
30
+ createdAt: '2026-01-02T00:00:00.000Z',
31
+ },
32
+ ];
33
+ mockFetch.mockResolvedValue(mockJsonResponse(txns));
34
+
35
+ const result = await client.getWalletTransactions();
36
+
37
+ expect(result).toEqual(txns);
38
+ const [url, init] = mockFetch.mock.calls[0];
39
+ expect(url).toBe('https://api.poll.fun/api/user/wallet/transactions');
40
+ expect(init.method).toBe('GET');
41
+ });
42
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "node",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src"],
15
+ "exclude": ["node_modules", "dist", "tests"]
16
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ export default defineConfig({
3
+ test: {
4
+ globals: true,
5
+ environment: 'node',
6
+ include: ['tests/**/*.test.ts'],
7
+ testTimeout: 15000,
8
+ },
9
+ });