@puul/partner-sdk 1.0.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,339 @@
1
+ interface PuulPartnerConfig {
2
+ /** Your partner client ID */
3
+ clientId: string;
4
+ /** Your partner client secret */
5
+ clientSecret: string;
6
+ /** Base URL for the Puul API. Defaults to https://api.joinpuul.com/api/v1 */
7
+ baseUrl?: string;
8
+ /** Request timeout in ms. Defaults to 30000 */
9
+ timeoutMs?: number;
10
+ }
11
+ interface AccessTokenResponse {
12
+ access_token: string;
13
+ token_type: string;
14
+ expires_in: number;
15
+ }
16
+ interface CreateLinkTokenParams {
17
+ externalUserId: string;
18
+ email?: string;
19
+ phone?: string;
20
+ countryCode?: string;
21
+ }
22
+ interface LinkTokenResponse {
23
+ linkToken: string;
24
+ expiresAt: string;
25
+ }
26
+ interface SessionResponse {
27
+ access_token: string;
28
+ token_type: string;
29
+ expires_in: number;
30
+ user_id: string;
31
+ }
32
+ interface MarketOutcome {
33
+ id: string;
34
+ slug: string;
35
+ label: string;
36
+ pool: {
37
+ pool_amount: string;
38
+ total_bets: number;
39
+ };
40
+ }
41
+ interface Market {
42
+ id: string;
43
+ question: string;
44
+ description: string;
45
+ category: string;
46
+ status: string;
47
+ currency: string;
48
+ close_time: string;
49
+ freeze_at: string | null;
50
+ target_countries: string[];
51
+ outcomes: MarketOutcome[];
52
+ }
53
+ type PredictionStatus = 'OPEN' | 'WON' | 'LOST' | 'VOIDED';
54
+ type Currency = 'NGN' | 'USDC' | 'USDT' | 'KES' | 'GHS' | 'ZAR';
55
+ interface PlacePredictionParams {
56
+ marketId: string;
57
+ outcomeId: string;
58
+ stakeAmount: number;
59
+ stakeCurrency: Currency;
60
+ idempotencyKey: string;
61
+ /** Optional quote ID for slippage protection */
62
+ quoteId?: string;
63
+ /** Optional FX quote ID */
64
+ fxQuoteId?: string;
65
+ /** Max slippage in basis points (100 = 1%) */
66
+ maxSlippageBps?: number;
67
+ }
68
+ interface CreateQuoteParams {
69
+ marketId: string;
70
+ outcomeId: string;
71
+ stakeAmount: number;
72
+ stakeCurrency: Currency;
73
+ }
74
+ interface QuoteResponse {
75
+ quoteId: string;
76
+ estimatedReturn: number;
77
+ multiplier: string;
78
+ expiresAt: string;
79
+ }
80
+ interface Prediction {
81
+ id: string;
82
+ market_id: string;
83
+ market_question: string | null;
84
+ market_close_time: string | null;
85
+ market_freeze_at: string | null;
86
+ outcome_id: string;
87
+ outcome_slug: string | null;
88
+ outcome_label: string | null;
89
+ user_external_id: string | null;
90
+ stake_amount: string;
91
+ stake_currency: string;
92
+ status: PredictionStatus;
93
+ estimated_return: string;
94
+ live_estimated_return: string | number;
95
+ estimated_multiplier: string;
96
+ final_return: string | null;
97
+ final_multiplier: string | null;
98
+ placed_at: string;
99
+ idempotency_key: string;
100
+ }
101
+ interface PredictionListResponse {
102
+ predictions: Prediction[];
103
+ total: number;
104
+ limit: number;
105
+ }
106
+ interface CreatePendingPredictionParams {
107
+ marketId: string;
108
+ outcomeId?: string;
109
+ /** @deprecated Use outcomeId for multi-outcome markets */
110
+ side?: 'YES' | 'NO';
111
+ stakeAmount: number;
112
+ stakeCurrency: Currency;
113
+ userExternalId: string;
114
+ idempotencyKey: string;
115
+ maxSlippageBps?: number;
116
+ /** TTL in seconds. Default: 300 */
117
+ ttlSeconds?: number;
118
+ }
119
+ interface PendingPrediction {
120
+ id: string;
121
+ status: string;
122
+ userExternalId: string;
123
+ marketId: string;
124
+ outcomeId: string | null;
125
+ side: string | null;
126
+ stakeAmount: number;
127
+ stakeCurrency: string;
128
+ paymentReference: string;
129
+ expiresAt: string;
130
+ predictionId: string | null;
131
+ createdAt: string;
132
+ fundedAt: string | null;
133
+ placedAt: string | null;
134
+ errorMessage: string | null;
135
+ }
136
+ interface WalletBalance {
137
+ balance: number;
138
+ currency: string;
139
+ }
140
+ interface DepositAccountInfo {
141
+ accountNumber: string;
142
+ accountName: string;
143
+ bankName: string;
144
+ currency: string;
145
+ }
146
+ interface DepositParams {
147
+ amount: number;
148
+ currency?: string;
149
+ idempotency_key: string;
150
+ reference?: string;
151
+ }
152
+ type WebhookEventType = 'prediction.settled' | 'deposit.confirmed' | 'withdrawal.completed';
153
+ interface WebhookEvent<T = {
154
+ [key: string]: unknown;
155
+ }> {
156
+ event: WebhookEventType;
157
+ timestamp: string;
158
+ data: T;
159
+ }
160
+ interface PredictionSettledData {
161
+ prediction_id: string;
162
+ market_id: string;
163
+ outcome_id: string;
164
+ outcome_slug: string;
165
+ user_external_id: string;
166
+ status: 'WON' | 'LOST' | 'VOIDED';
167
+ stake_amount: string;
168
+ stake_currency: string;
169
+ final_return: string;
170
+ settled_at: string;
171
+ }
172
+ interface PuulApiError {
173
+ statusCode: number;
174
+ message: string | string[];
175
+ code: string;
176
+ requestId?: string;
177
+ details?: unknown;
178
+ retryable?: boolean;
179
+ }
180
+ declare class PuulError extends Error {
181
+ readonly statusCode: number;
182
+ readonly code: string;
183
+ readonly requestId?: string;
184
+ readonly details?: unknown;
185
+ readonly retryable: boolean;
186
+ constructor(apiError: PuulApiError);
187
+ }
188
+
189
+ /**
190
+ * Official Puul Partner SDK client.
191
+ *
192
+ * Zero runtime dependencies — uses native `fetch` (Node 18+).
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * import { PuulPartner } from '@puul/partner-sdk';
197
+ *
198
+ * const puul = new PuulPartner({
199
+ * clientId: 'pk_live_abc123',
200
+ * clientSecret: 'sk_live_secret789',
201
+ * });
202
+ *
203
+ * // List markets
204
+ * const markets = await puul.markets.list();
205
+ *
206
+ * // Place a prediction
207
+ * const prediction = await puul.predictions.place({
208
+ * marketId: markets[0].id,
209
+ * outcomeId: markets[0].outcomes[0].id,
210
+ * stakeAmount: 100000,
211
+ * stakeCurrency: 'NGN',
212
+ * idempotencyKey: 'unique-key-123',
213
+ * });
214
+ * ```
215
+ */
216
+ declare class PuulPartner {
217
+ private readonly config;
218
+ private tokenCache;
219
+ readonly sessions: SessionsAPI;
220
+ readonly markets: MarketsAPI;
221
+ readonly predictions: PredictionsAPI;
222
+ readonly wallet: WalletAPI;
223
+ constructor(config: PuulPartnerConfig);
224
+ /** Get a valid access token, refreshing if needed */
225
+ getAccessToken(): Promise<string>;
226
+ /** Authenticated request */
227
+ request<T>(method: string, path: string, body?: unknown): Promise<T>;
228
+ /** Raw HTTP request */
229
+ private rawRequest;
230
+ }
231
+ declare class SessionsAPI {
232
+ private readonly client;
233
+ constructor(client: PuulPartner);
234
+ /** Create a link token for user linking */
235
+ createLinkToken(params: CreateLinkTokenParams): Promise<LinkTokenResponse>;
236
+ /** Exchange a link token for a user session */
237
+ create(linkToken: string): Promise<SessionResponse>;
238
+ }
239
+ declare class MarketsAPI {
240
+ private readonly client;
241
+ constructor(client: PuulPartner);
242
+ /** List all live markets, optionally filtered by country */
243
+ list(countries?: string[]): Promise<Market[]>;
244
+ }
245
+ declare class PredictionsAPI {
246
+ private readonly client;
247
+ constructor(client: PuulPartner);
248
+ /** Create a binding quote for slippage protection (expires in 10s) */
249
+ createQuote(params: CreateQuoteParams): Promise<QuoteResponse>;
250
+ /** Place a prediction on a market outcome */
251
+ place(params: PlacePredictionParams): Promise<Prediction>;
252
+ /** Get a specific prediction by ID */
253
+ get(predictionId: string): Promise<Prediction>;
254
+ /** List user predictions with optional filters */
255
+ list(options?: {
256
+ status?: PredictionStatus;
257
+ limit?: number;
258
+ }): Promise<PredictionListResponse>;
259
+ /** Create a pending prediction (JIT funding flow) */
260
+ createPending(params: CreatePendingPredictionParams): Promise<PendingPrediction>;
261
+ /** Get pending prediction status */
262
+ getPending(pendingId: string): Promise<PendingPrediction>;
263
+ }
264
+ declare class WalletAPI {
265
+ private readonly client;
266
+ constructor(client: PuulPartner);
267
+ /** Get partner deposit account details */
268
+ getDepositAccount(): Promise<DepositAccountInfo>;
269
+ /** Get user wallet balance */
270
+ getBalance(): Promise<WalletBalance>;
271
+ /** Get omnibus wallet balance */
272
+ getOmnibusBalance(currency?: string): Promise<WalletBalance>;
273
+ /** Deposit funds to a linked user wallet */
274
+ deposit(params: DepositParams): Promise<unknown>;
275
+ /** Get withdrawal fee estimate */
276
+ getWithdrawalFees(currency: string, amount: number): Promise<unknown>;
277
+ }
278
+
279
+ /**
280
+ * Verify a webhook signature from Puul.
281
+ *
282
+ * @param payload - The raw request body as a string
283
+ * @param signature - The value of the X-Puul-Signature header
284
+ * @param secret - Your webhook secret
285
+ * @returns true if the signature is valid
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * import { verifyWebhookSignature } from '@puul/partner-sdk';
290
+ *
291
+ * app.post('/webhooks/puul', (req, res) => {
292
+ * const isValid = verifyWebhookSignature(
293
+ * req.body, // raw string body
294
+ * req.headers['x-puul-signature'],
295
+ * process.env.PUUL_WEBHOOK_SECRET,
296
+ * );
297
+ *
298
+ * if (!isValid) {
299
+ * return res.status(401).send('Invalid signature');
300
+ * }
301
+ *
302
+ * // Process the event...
303
+ * res.status(200).send('OK');
304
+ * });
305
+ * ```
306
+ */
307
+ declare function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean;
308
+ /**
309
+ * Parse and type a webhook event body.
310
+ *
311
+ * @param body - The parsed JSON body of the webhook request
312
+ * @returns A typed WebhookEvent object
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * import { parseWebhookEvent } from '@puul/partner-sdk';
317
+ *
318
+ * const event = parseWebhookEvent(req.body);
319
+ *
320
+ * if (event.event === 'prediction.settled') {
321
+ * const { prediction_id, status, final_return } = event.data;
322
+ * // Handle settlement...
323
+ * }
324
+ * ```
325
+ */
326
+ declare function parseWebhookEvent(body: unknown): WebhookEvent;
327
+ /**
328
+ * Type guard for prediction.settled events.
329
+ * Returns true if the event type is 'prediction.settled'.
330
+ * Use this to narrow the type before accessing `.data` as PredictionSettledData.
331
+ */
332
+ declare function isPredictionSettledEvent(event: WebhookEvent): boolean;
333
+ /**
334
+ * Convenience cast for prediction.settled event data.
335
+ * Call after verifying with `isPredictionSettledEvent`.
336
+ */
337
+ declare function asPredictionSettledData(event: WebhookEvent): PredictionSettledData;
338
+
339
+ export { type AccessTokenResponse, type CreateLinkTokenParams, type CreatePendingPredictionParams, type CreateQuoteParams, type Currency, type DepositAccountInfo, type DepositParams, type LinkTokenResponse, type Market, type MarketOutcome, type PendingPrediction, type PlacePredictionParams, type Prediction, type PredictionListResponse, type PredictionSettledData, type PredictionStatus, type PuulApiError, PuulError, PuulPartner, type PuulPartnerConfig, type QuoteResponse, type SessionResponse, type WalletBalance, type WebhookEvent, type WebhookEventType, asPredictionSettledData, isPredictionSettledEvent, parseWebhookEvent, verifyWebhookSignature };
package/dist/index.js ADDED
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ PuulError: () => PuulError,
24
+ PuulPartner: () => PuulPartner,
25
+ asPredictionSettledData: () => asPredictionSettledData,
26
+ isPredictionSettledEvent: () => isPredictionSettledEvent,
27
+ parseWebhookEvent: () => parseWebhookEvent,
28
+ verifyWebhookSignature: () => verifyWebhookSignature
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/types.ts
33
+ var PuulError = class extends Error {
34
+ constructor(apiError) {
35
+ const msg = Array.isArray(apiError.message) ? apiError.message.join(", ") : apiError.message;
36
+ super(msg);
37
+ this.name = "PuulError";
38
+ this.statusCode = apiError.statusCode;
39
+ this.code = apiError.code;
40
+ this.requestId = apiError.requestId;
41
+ this.details = apiError.details;
42
+ this.retryable = apiError.retryable ?? false;
43
+ }
44
+ };
45
+
46
+ // src/client.ts
47
+ var DEFAULT_BASE_URL = "https://api.joinpuul.com/api/v1";
48
+ var DEFAULT_TIMEOUT_MS = 3e4;
49
+ var PuulPartner = class {
50
+ constructor(config) {
51
+ this.tokenCache = null;
52
+ this.config = {
53
+ clientId: config.clientId,
54
+ clientSecret: config.clientSecret,
55
+ baseUrl: (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, ""),
56
+ timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
57
+ };
58
+ this.sessions = new SessionsAPI(this);
59
+ this.markets = new MarketsAPI(this);
60
+ this.predictions = new PredictionsAPI(this);
61
+ this.wallet = new WalletAPI(this);
62
+ }
63
+ // ==========================================================
64
+ // Internal HTTP layer
65
+ // ==========================================================
66
+ /** Get a valid access token, refreshing if needed */
67
+ async getAccessToken() {
68
+ if (this.tokenCache && Date.now() < this.tokenCache.expiresAt - 3e4) {
69
+ return this.tokenCache.accessToken;
70
+ }
71
+ const response = await this.rawRequest(
72
+ "POST",
73
+ "/partner/oauth/token",
74
+ {
75
+ client_id: this.config.clientId,
76
+ client_secret: this.config.clientSecret,
77
+ grant_type: "client_credentials"
78
+ },
79
+ false
80
+ // Don't use auth for the auth endpoint itself
81
+ );
82
+ this.tokenCache = {
83
+ accessToken: response.access_token,
84
+ expiresAt: Date.now() + response.expires_in * 1e3
85
+ };
86
+ return this.tokenCache.accessToken;
87
+ }
88
+ /** Authenticated request */
89
+ async request(method, path, body) {
90
+ return this.rawRequest(method, path, body, true);
91
+ }
92
+ /** Raw HTTP request */
93
+ async rawRequest(method, path, body, authenticate = true) {
94
+ const url = `${this.config.baseUrl}${path}`;
95
+ const headers = {
96
+ "Content-Type": "application/json",
97
+ "Accept": "application/json"
98
+ };
99
+ if (authenticate) {
100
+ const token = await this.getAccessToken();
101
+ headers["Authorization"] = `Bearer ${token}`;
102
+ }
103
+ const controller = new AbortController();
104
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
105
+ try {
106
+ const response = await fetch(url, {
107
+ method,
108
+ headers,
109
+ body: body ? JSON.stringify(body) : void 0,
110
+ signal: controller.signal
111
+ });
112
+ const responseBody = await response.text();
113
+ let parsed;
114
+ try {
115
+ parsed = JSON.parse(responseBody);
116
+ } catch {
117
+ if (!response.ok) {
118
+ throw new PuulError({
119
+ statusCode: response.status,
120
+ message: responseBody || response.statusText,
121
+ code: "UNKNOWN_ERROR"
122
+ });
123
+ }
124
+ return responseBody;
125
+ }
126
+ if (!response.ok) {
127
+ throw new PuulError(parsed);
128
+ }
129
+ return parsed;
130
+ } catch (error) {
131
+ if (error instanceof PuulError) throw error;
132
+ if (error instanceof Error && error.name === "AbortError") {
133
+ throw new PuulError({
134
+ statusCode: 0,
135
+ message: `Request timed out after ${this.config.timeoutMs}ms`,
136
+ code: "REQUEST_TIMEOUT",
137
+ retryable: true
138
+ });
139
+ }
140
+ throw new PuulError({
141
+ statusCode: 0,
142
+ message: error instanceof Error ? error.message : "Unknown network error",
143
+ code: "NETWORK_ERROR",
144
+ retryable: true
145
+ });
146
+ } finally {
147
+ clearTimeout(timeout);
148
+ }
149
+ }
150
+ };
151
+ var SessionsAPI = class {
152
+ constructor(client) {
153
+ this.client = client;
154
+ }
155
+ /** Create a link token for user linking */
156
+ async createLinkToken(params) {
157
+ return this.client.request("POST", "/partner/link-tokens", params);
158
+ }
159
+ /** Exchange a link token for a user session */
160
+ async create(linkToken) {
161
+ return this.client.request("POST", "/partner/sessions", { linkToken });
162
+ }
163
+ };
164
+ var MarketsAPI = class {
165
+ constructor(client) {
166
+ this.client = client;
167
+ }
168
+ /** List all live markets, optionally filtered by country */
169
+ async list(countries) {
170
+ const query = countries?.length ? `?countries=${countries.join(",")}` : "";
171
+ return this.client.request("GET", `/partner/markets${query}`);
172
+ }
173
+ };
174
+ var PredictionsAPI = class {
175
+ constructor(client) {
176
+ this.client = client;
177
+ }
178
+ /** Create a binding quote for slippage protection (expires in 10s) */
179
+ async createQuote(params) {
180
+ return this.client.request("POST", "/partner/predictions/quote", params);
181
+ }
182
+ /** Place a prediction on a market outcome */
183
+ async place(params) {
184
+ return this.client.request("POST", "/partner/predictions", params);
185
+ }
186
+ /** Get a specific prediction by ID */
187
+ async get(predictionId) {
188
+ return this.client.request("GET", `/partner/predictions/${predictionId}`);
189
+ }
190
+ /** List user predictions with optional filters */
191
+ async list(options) {
192
+ const params = new URLSearchParams();
193
+ if (options?.status) params.set("status", options.status);
194
+ if (options?.limit) params.set("limit", String(options.limit));
195
+ const query = params.toString() ? `?${params.toString()}` : "";
196
+ return this.client.request("GET", `/partner/predictions${query}`);
197
+ }
198
+ /** Create a pending prediction (JIT funding flow) */
199
+ async createPending(params) {
200
+ return this.client.request("POST", "/partner/predictions/pending", params);
201
+ }
202
+ /** Get pending prediction status */
203
+ async getPending(pendingId) {
204
+ return this.client.request("GET", `/partner/predictions/pending/${pendingId}`);
205
+ }
206
+ };
207
+ var WalletAPI = class {
208
+ constructor(client) {
209
+ this.client = client;
210
+ }
211
+ /** Get partner deposit account details */
212
+ async getDepositAccount() {
213
+ return this.client.request("GET", "/partner/wallet/deposit-account");
214
+ }
215
+ /** Get user wallet balance */
216
+ async getBalance() {
217
+ return this.client.request("GET", "/partner/wallet/balance");
218
+ }
219
+ /** Get omnibus wallet balance */
220
+ async getOmnibusBalance(currency) {
221
+ const query = currency ? `?currency=${currency}` : "";
222
+ return this.client.request("GET", `/partner/wallet/omnibus-balance${query}`);
223
+ }
224
+ /** Deposit funds to a linked user wallet */
225
+ async deposit(params) {
226
+ return this.client.request("POST", "/partner/wallet/deposit", params);
227
+ }
228
+ /** Get withdrawal fee estimate */
229
+ async getWithdrawalFees(currency, amount) {
230
+ return this.client.request("GET", `/partner/wallet/withdrawal-fees?currency=${currency}&amount=${amount}`);
231
+ }
232
+ };
233
+
234
+ // src/webhooks.ts
235
+ var import_crypto = require("crypto");
236
+ function verifyWebhookSignature(payload, signature, secret) {
237
+ if (!payload || !signature || !secret) {
238
+ return false;
239
+ }
240
+ const expectedSignature = (0, import_crypto.createHmac)("sha256", secret).update(payload).digest("hex");
241
+ try {
242
+ const sigBuffer = Buffer.from(signature, "hex");
243
+ const expectedBuffer = Buffer.from(expectedSignature, "hex");
244
+ if (sigBuffer.length !== expectedBuffer.length) {
245
+ return false;
246
+ }
247
+ return (0, import_crypto.timingSafeEqual)(sigBuffer, expectedBuffer);
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
252
+ function parseWebhookEvent(body) {
253
+ if (!body || typeof body !== "object") {
254
+ throw new Error("Invalid webhook body: expected an object");
255
+ }
256
+ const event = body;
257
+ if (typeof event.event !== "string") {
258
+ throw new Error('Invalid webhook body: missing "event" field');
259
+ }
260
+ return {
261
+ event: event.event,
262
+ timestamp: event.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
263
+ data: event.data || {}
264
+ };
265
+ }
266
+ function isPredictionSettledEvent(event) {
267
+ return event.event === "prediction.settled";
268
+ }
269
+ function asPredictionSettledData(event) {
270
+ return event.data;
271
+ }
272
+ // Annotate the CommonJS export names for ESM import in node:
273
+ 0 && (module.exports = {
274
+ PuulError,
275
+ PuulPartner,
276
+ asPredictionSettledData,
277
+ isPredictionSettledEvent,
278
+ parseWebhookEvent,
279
+ verifyWebhookSignature
280
+ });