@rotateprotocol/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.
Files changed (74) hide show
  1. package/README.md +453 -0
  2. package/dist/catalog.d.ts +112 -0
  3. package/dist/catalog.d.ts.map +1 -0
  4. package/dist/catalog.js +210 -0
  5. package/dist/catalog.js.map +1 -0
  6. package/dist/components/CheckoutForm.d.ts +86 -0
  7. package/dist/components/CheckoutForm.d.ts.map +1 -0
  8. package/dist/components/CheckoutForm.js +332 -0
  9. package/dist/components/CheckoutForm.js.map +1 -0
  10. package/dist/components/HostedCheckout.d.ts +57 -0
  11. package/dist/components/HostedCheckout.d.ts.map +1 -0
  12. package/dist/components/HostedCheckout.js +414 -0
  13. package/dist/components/HostedCheckout.js.map +1 -0
  14. package/dist/components/PaymentButton.d.ts +80 -0
  15. package/dist/components/PaymentButton.d.ts.map +1 -0
  16. package/dist/components/PaymentButton.js +210 -0
  17. package/dist/components/PaymentButton.js.map +1 -0
  18. package/dist/components/RotateProvider.d.ts +115 -0
  19. package/dist/components/RotateProvider.d.ts.map +1 -0
  20. package/dist/components/RotateProvider.js +264 -0
  21. package/dist/components/RotateProvider.js.map +1 -0
  22. package/dist/components/index.d.ts +17 -0
  23. package/dist/components/index.d.ts.map +1 -0
  24. package/dist/components/index.js +27 -0
  25. package/dist/components/index.js.map +1 -0
  26. package/dist/embed.d.ts +85 -0
  27. package/dist/embed.d.ts.map +1 -0
  28. package/dist/embed.js +313 -0
  29. package/dist/embed.js.map +1 -0
  30. package/dist/hooks.d.ts +156 -0
  31. package/dist/hooks.d.ts.map +1 -0
  32. package/dist/hooks.js +280 -0
  33. package/dist/hooks.js.map +1 -0
  34. package/dist/idl/rotate_connect.json +2572 -0
  35. package/dist/index.d.ts +505 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +1197 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/marketplace.d.ts +257 -0
  40. package/dist/marketplace.d.ts.map +1 -0
  41. package/dist/marketplace.js +433 -0
  42. package/dist/marketplace.js.map +1 -0
  43. package/dist/platform.d.ts +234 -0
  44. package/dist/platform.d.ts.map +1 -0
  45. package/dist/platform.js +268 -0
  46. package/dist/platform.js.map +1 -0
  47. package/dist/react.d.ts +140 -0
  48. package/dist/react.d.ts.map +1 -0
  49. package/dist/react.js +429 -0
  50. package/dist/react.js.map +1 -0
  51. package/dist/store.d.ts +213 -0
  52. package/dist/store.d.ts.map +1 -0
  53. package/dist/store.js +404 -0
  54. package/dist/store.js.map +1 -0
  55. package/dist/webhooks.d.ts +149 -0
  56. package/dist/webhooks.d.ts.map +1 -0
  57. package/dist/webhooks.js +371 -0
  58. package/dist/webhooks.js.map +1 -0
  59. package/package.json +114 -0
  60. package/src/catalog.ts +299 -0
  61. package/src/components/CheckoutForm.tsx +608 -0
  62. package/src/components/HostedCheckout.tsx +675 -0
  63. package/src/components/PaymentButton.tsx +348 -0
  64. package/src/components/RotateProvider.tsx +370 -0
  65. package/src/components/index.ts +26 -0
  66. package/src/embed.ts +408 -0
  67. package/src/hooks.ts +518 -0
  68. package/src/idl/rotate_connect.json +2572 -0
  69. package/src/index.ts +1538 -0
  70. package/src/marketplace.ts +642 -0
  71. package/src/platform.ts +403 -0
  72. package/src/react.ts +459 -0
  73. package/src/store.ts +577 -0
  74. package/src/webhooks.ts +506 -0
@@ -0,0 +1,506 @@
1
+ /**
2
+ * Rotate Protocol Webhooks
3
+ *
4
+ * Listen to on-chain payment events and receive notifications.
5
+ * This does NOT change the P2P nature - webhooks are notifications only.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import { Connection, PublicKey } from '@solana/web3.js';
11
+ import { BorshCoder } from '@coral-xyz/anchor';
12
+ import { PROGRAM_ID, Network } from './index';
13
+ import IDL from './idl/rotate_connect.json';
14
+
15
+ // ==================== TYPES ====================
16
+
17
+ export type WebhookEventType =
18
+ | 'protocol.initialized'
19
+ | 'protocol.updated'
20
+ | 'platform.created'
21
+ | 'platform.updated'
22
+ | 'merchant.created'
23
+ | 'merchant.updated'
24
+ | 'payment.completed'
25
+ | 'payment.pending'
26
+ | 'link.created'
27
+ | 'link.created.usd'
28
+ | 'link.paid'
29
+ | 'link.paid.usd'
30
+ | 'link.cancelled'
31
+ | 'link.closed'
32
+ | 'link.expired';
33
+
34
+ export interface WebhookEvent {
35
+ /** Unique event ID */
36
+ id: string;
37
+ /** Event type */
38
+ type: WebhookEventType;
39
+ /** Timestamp */
40
+ timestamp: number;
41
+ /** Transaction signature */
42
+ transactionSignature: string;
43
+ /** Event data */
44
+ data: WebhookEventData;
45
+ }
46
+
47
+ export interface WebhookEventData {
48
+ linkId?: number;
49
+ merchantId?: number;
50
+ platformId?: number;
51
+ payerWallet?: string;
52
+ merchantWallet?: string;
53
+ platformWallet?: string;
54
+ treasuryWallet?: string;
55
+ amountUsd?: number;
56
+ amountLamports?: number;
57
+ amountTokens?: number;
58
+ protocolFee?: number;
59
+ platformFee?: number;
60
+ tipAmount?: number;
61
+ currency?: 'SOL' | 'USDC' | 'USDT' | 'USD';
62
+ tokenType?: string;
63
+ orderRef?: string;
64
+ status?: string;
65
+ active?: boolean;
66
+ feeBps?: number;
67
+ }
68
+
69
+ export interface WebhookConfig {
70
+ /** Webhook endpoint URL */
71
+ url: string;
72
+ /** Events to subscribe to */
73
+ events: WebhookEventType[];
74
+ /** Filter by merchant ID (optional) */
75
+ merchantId?: number;
76
+ /** Filter by platform ID (optional) */
77
+ platformId?: number;
78
+ /** Secret for HMAC signature verification */
79
+ secret: string;
80
+ }
81
+
82
+ export type EventHandler = (event: WebhookEvent) => void | Promise<void>;
83
+
84
+ // ==================== SIGNATURE VERIFICATION ====================
85
+
86
+ /**
87
+ * Verify webhook signature (for webhook receivers)
88
+ */
89
+ export async function verifyWebhookSignature(
90
+ payload: string,
91
+ signature: string,
92
+ secret: string
93
+ ): Promise<boolean> {
94
+ const encoder = new TextEncoder();
95
+ const key = await crypto.subtle.importKey(
96
+ 'raw',
97
+ encoder.encode(secret),
98
+ { name: 'HMAC', hash: 'SHA-256' },
99
+ false,
100
+ ['sign']
101
+ );
102
+
103
+ const expectedSig = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
104
+ const expectedHex = Array.from(new Uint8Array(expectedSig))
105
+ .map(b => b.toString(16).padStart(2, '0'))
106
+ .join('');
107
+
108
+ return signature === expectedHex;
109
+ }
110
+
111
+ /**
112
+ * Create webhook signature (for webhook senders)
113
+ */
114
+ export async function createWebhookSignature(payload: string, secret: string): Promise<string> {
115
+ const encoder = new TextEncoder();
116
+ const key = await crypto.subtle.importKey(
117
+ 'raw',
118
+ encoder.encode(secret),
119
+ { name: 'HMAC', hash: 'SHA-256' },
120
+ false,
121
+ ['sign']
122
+ );
123
+
124
+ const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
125
+ return Array.from(new Uint8Array(signature))
126
+ .map(b => b.toString(16).padStart(2, '0'))
127
+ .join('');
128
+ }
129
+
130
+ // ==================== EVENT LISTENER ====================
131
+
132
+ export interface EventListenerOptions {
133
+ /** Solana network */
134
+ network: Network;
135
+ /** Custom RPC endpoint */
136
+ rpcEndpoint?: string;
137
+ /** Poll interval in ms (default: 5000) */
138
+ pollIntervalMs?: number;
139
+ /** Filter by merchant ID */
140
+ merchantId?: number;
141
+ /** Filter by platform ID */
142
+ platformId?: number;
143
+ }
144
+
145
+ /**
146
+ * On-chain event listener
147
+ * Polls the blockchain for new transactions and emits events
148
+ */
149
+ export class EventListener {
150
+ private connection: Connection;
151
+ private programId: PublicKey;
152
+ private options: EventListenerOptions;
153
+ private handlers: Map<WebhookEventType, EventHandler[]> = new Map();
154
+ private lastSignature: string | null = null;
155
+ private isRunning = false;
156
+ private pollInterval: NodeJS.Timeout | null = null;
157
+ private coder: BorshCoder;
158
+
159
+ constructor(options: EventListenerOptions) {
160
+ this.options = options;
161
+ this.programId = PROGRAM_ID;
162
+ this.coder = new BorshCoder(IDL as any);
163
+
164
+ const endpoint = options.rpcEndpoint ||
165
+ (options.network === 'mainnet-beta'
166
+ ? 'https://api.mainnet-beta.solana.com'
167
+ : 'https://api.devnet.solana.com');
168
+
169
+ this.connection = new Connection(endpoint, 'confirmed');
170
+ }
171
+
172
+ /**
173
+ * Subscribe to an event type
174
+ */
175
+ on(eventType: WebhookEventType, handler: EventHandler): void {
176
+ const handlers = this.handlers.get(eventType) || [];
177
+ handlers.push(handler);
178
+ this.handlers.set(eventType, handlers);
179
+ }
180
+
181
+ /**
182
+ * Unsubscribe from an event type
183
+ */
184
+ off(eventType: WebhookEventType, handler: EventHandler): void {
185
+ const handlers = this.handlers.get(eventType) || [];
186
+ const index = handlers.indexOf(handler);
187
+ if (index > -1) {
188
+ handlers.splice(index, 1);
189
+ this.handlers.set(eventType, handlers);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Subscribe to all events
195
+ */
196
+ onAny(handler: EventHandler): void {
197
+ const allTypes: WebhookEventType[] = [
198
+ 'protocol.initialized',
199
+ 'protocol.updated',
200
+ 'platform.created',
201
+ 'platform.updated',
202
+ 'merchant.created',
203
+ 'merchant.updated',
204
+ 'payment.completed',
205
+ 'payment.pending',
206
+ 'link.created',
207
+ 'link.created.usd',
208
+ 'link.paid',
209
+ 'link.paid.usd',
210
+ 'link.cancelled',
211
+ 'link.closed',
212
+ 'link.expired',
213
+ ];
214
+ allTypes.forEach(type => this.on(type, handler));
215
+ }
216
+
217
+ /**
218
+ * Start listening for events
219
+ */
220
+ start(): void {
221
+ if (this.isRunning) return;
222
+ this.isRunning = true;
223
+
224
+ this.poll();
225
+ this.pollInterval = setInterval(
226
+ () => this.poll(),
227
+ this.options.pollIntervalMs || 5000
228
+ );
229
+ }
230
+
231
+ /**
232
+ * Stop listening for events
233
+ */
234
+ stop(): void {
235
+ this.isRunning = false;
236
+ if (this.pollInterval) {
237
+ clearInterval(this.pollInterval);
238
+ this.pollInterval = null;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Poll for new transactions
244
+ */
245
+ private async poll(): Promise<void> {
246
+ try {
247
+ const signatures = await this.connection.getSignaturesForAddress(
248
+ this.programId,
249
+ {
250
+ limit: 20,
251
+ until: this.lastSignature || undefined,
252
+ }
253
+ );
254
+
255
+ if (signatures.length === 0) return;
256
+
257
+ // Update last signature
258
+ this.lastSignature = signatures[0].signature;
259
+
260
+ // Process transactions in order (oldest first)
261
+ for (const sig of signatures.reverse()) {
262
+ await this.processTransaction(sig.signature);
263
+ }
264
+ } catch (error) {
265
+ console.error('Error polling for events:', error);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Process a transaction and emit decoded events
271
+ */
272
+ private async processTransaction(signature: string): Promise<void> {
273
+ try {
274
+ const tx = await this.connection.getParsedTransaction(signature, {
275
+ maxSupportedTransactionVersion: 0,
276
+ });
277
+
278
+ if (!tx?.meta?.logMessages) return;
279
+
280
+ const events: WebhookEvent[] = [];
281
+ const timestamp = tx.blockTime ? tx.blockTime * 1000 : Date.now();
282
+
283
+ // Decode Anchor events from "Program data:" log entries
284
+ for (const log of tx.meta.logMessages) {
285
+ if (!log.startsWith('Program data: ')) continue;
286
+
287
+ try {
288
+ const base64Data = log.slice('Program data: '.length);
289
+ const decoded = this.coder.events.decode(base64Data);
290
+ if (!decoded) continue;
291
+
292
+ const webhookType = this.anchorEventToWebhookType(decoded.name);
293
+ if (!webhookType) continue;
294
+
295
+ events.push({
296
+ id: `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
297
+ type: webhookType,
298
+ timestamp,
299
+ transactionSignature: signature,
300
+ data: this.mapEventData(decoded.name, decoded.data),
301
+ });
302
+ } catch {
303
+ // Skip events that can't be decoded
304
+ }
305
+ }
306
+
307
+ for (const event of events) {
308
+ // Apply filters
309
+ if (this.options.merchantId && event.data.merchantId !== this.options.merchantId) {
310
+ continue;
311
+ }
312
+ if (this.options.platformId && event.data.platformId !== this.options.platformId) {
313
+ continue;
314
+ }
315
+
316
+ // Emit to handlers
317
+ await this.emit(event);
318
+ }
319
+ } catch (error) {
320
+ console.error('Error processing transaction:', error);
321
+ }
322
+ }
323
+
324
+ /** Map Anchor event name to webhook event type */
325
+ private anchorEventToWebhookType(name: string): WebhookEventType | null {
326
+ const map: Record<string, WebhookEventType> = {
327
+ 'ProtocolInitialized': 'protocol.initialized',
328
+ 'ProtocolUpdated': 'protocol.updated',
329
+ 'PlatformCreated': 'platform.created',
330
+ 'PlatformUpdated': 'platform.updated',
331
+ 'MerchantCreated': 'merchant.created',
332
+ 'MerchantUpdated': 'merchant.updated',
333
+ 'PaymentProcessed': 'payment.completed',
334
+ 'LinkCreated': 'link.created',
335
+ 'LinkCreatedUsd': 'link.created.usd',
336
+ 'LinkPaid': 'link.paid',
337
+ 'LinkPaidUsd': 'link.paid.usd',
338
+ 'LinkCancelled': 'link.cancelled',
339
+ 'LinkClosed': 'link.closed',
340
+ };
341
+ return map[name] || null;
342
+ }
343
+
344
+ /** Map decoded Anchor event data to WebhookEventData */
345
+ private mapEventData(eventName: string, data: any): WebhookEventData {
346
+ const result: WebhookEventData = {};
347
+
348
+ // ID field: link ID for Link* events, platform/merchant ID for others
349
+ if (data.id !== undefined) {
350
+ if (eventName.startsWith('Link')) {
351
+ result.linkId = Number(data.id);
352
+ } else if (eventName.startsWith('Platform')) {
353
+ result.platformId = Number(data.id);
354
+ } else if (eventName.startsWith('Merchant')) {
355
+ result.merchantId = Number(data.id);
356
+ }
357
+ }
358
+
359
+ // Common entity IDs (Anchor camelCases snake_case fields)
360
+ if (data.merchantId !== undefined) result.merchantId = Number(data.merchantId);
361
+ if (data.platformId !== undefined) result.platformId = Number(data.platformId);
362
+
363
+ // Wallet addresses
364
+ if (data.payer) result.payerWallet = data.payer.toString();
365
+ if (data.wallet) result.merchantWallet = data.wallet.toString();
366
+ if (data.admin) result.platformWallet = data.admin.toString();
367
+ if (data.treasury) result.treasuryWallet = data.treasury.toString();
368
+
369
+ // Amounts
370
+ if (data.amount !== undefined) result.amountLamports = Number(data.amount);
371
+ if (data.amountUsd !== undefined) result.amountUsd = Number(data.amountUsd) / 1_000_000;
372
+ if (data.usdAmount !== undefined) result.amountUsd = Number(data.usdAmount) / 1_000_000;
373
+ if (data.merchantAmount !== undefined) result.amountTokens = Number(data.merchantAmount);
374
+
375
+ // Fees
376
+ if (data.protocolFee !== undefined) result.protocolFee = Number(data.protocolFee);
377
+ if (data.platformFee !== undefined) result.platformFee = Number(data.platformFee);
378
+ if (data.feeBps !== undefined) result.feeBps = Number(data.feeBps);
379
+
380
+ // Tips
381
+ if (data.tip !== undefined) result.tipAmount = Number(data.tip);
382
+ if (data.tipUsd !== undefined) result.tipAmount = Number(data.tipUsd) / 1_000_000;
383
+
384
+ // Order reference
385
+ if (data.orderRef !== undefined) result.orderRef = data.orderRef.toString();
386
+
387
+ // Token type (Anchor deserializes enums as { variant: {} })
388
+ if (data.tokenType !== undefined) {
389
+ const variant = typeof data.tokenType === 'object' ? Object.keys(data.tokenType)[0] : String(data.tokenType);
390
+ result.tokenType = variant;
391
+ if (variant === 'sol' || variant === 'Sol') result.currency = 'SOL';
392
+ else if (variant === 'usdc' || variant === 'Usdc') result.currency = 'USDC';
393
+ else if (variant === 'usdt' || variant === 'Usdt') result.currency = 'USDT';
394
+ else if (variant === 'usd' || variant === 'Usd') result.currency = 'USD';
395
+ }
396
+
397
+ // Status (same enum pattern)
398
+ if (data.status !== undefined) {
399
+ result.status = typeof data.status === 'object' ? Object.keys(data.status)[0] : String(data.status);
400
+ }
401
+
402
+ // Active flag
403
+ if (data.active !== undefined) result.active = Boolean(data.active);
404
+
405
+ return result;
406
+ }
407
+
408
+ /**
409
+ * Emit event to handlers
410
+ */
411
+ private async emit(event: WebhookEvent): Promise<void> {
412
+ const handlers = this.handlers.get(event.type) || [];
413
+
414
+ for (const handler of handlers) {
415
+ try {
416
+ await handler(event);
417
+ } catch (error) {
418
+ console.error('Error in event handler:', error);
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ // ==================== WEBHOOK SENDER ====================
425
+
426
+ export interface WebhookSenderOptions {
427
+ /** Retry failed webhooks */
428
+ retryCount?: number;
429
+ /** Retry delay in ms */
430
+ retryDelayMs?: number;
431
+ /** Timeout in ms */
432
+ timeoutMs?: number;
433
+ }
434
+
435
+ /**
436
+ * Send webhooks to registered endpoints
437
+ */
438
+ export class WebhookSender {
439
+ private options: WebhookSenderOptions;
440
+
441
+ constructor(options: WebhookSenderOptions = {}) {
442
+ this.options = {
443
+ retryCount: options.retryCount ?? 3,
444
+ retryDelayMs: options.retryDelayMs ?? 1000,
445
+ timeoutMs: options.timeoutMs ?? 10000,
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Send webhook to endpoint
451
+ */
452
+ async send(config: WebhookConfig, event: WebhookEvent): Promise<boolean> {
453
+ const payload = JSON.stringify(event);
454
+ const signature = await createWebhookSignature(payload, config.secret);
455
+
456
+ for (let attempt = 0; attempt <= this.options.retryCount!; attempt++) {
457
+ try {
458
+ const controller = new AbortController();
459
+ const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs);
460
+
461
+ const response = await fetch(config.url, {
462
+ method: 'POST',
463
+ headers: {
464
+ 'Content-Type': 'application/json',
465
+ 'X-Rotate-Signature': signature,
466
+ 'X-Rotate-Event': event.type,
467
+ 'X-Rotate-Timestamp': event.timestamp.toString(),
468
+ 'X-Rotate-Delivery-Attempt': attempt.toString(),
469
+ },
470
+ body: payload,
471
+ signal: controller.signal,
472
+ });
473
+
474
+ clearTimeout(timeout);
475
+
476
+ if (response.ok) {
477
+ return true;
478
+ }
479
+
480
+ // Retry on 5xx errors
481
+ if (response.status >= 500 && attempt < this.options.retryCount!) {
482
+ await this.delay(this.options.retryDelayMs! * Math.pow(2, attempt));
483
+ continue;
484
+ }
485
+
486
+ return false;
487
+ } catch (error) {
488
+ if (attempt < this.options.retryCount!) {
489
+ await this.delay(this.options.retryDelayMs! * Math.pow(2, attempt));
490
+ continue;
491
+ }
492
+ return false;
493
+ }
494
+ }
495
+
496
+ return false;
497
+ }
498
+
499
+ private delay(ms: number): Promise<void> {
500
+ return new Promise(resolve => setTimeout(resolve, ms));
501
+ }
502
+ }
503
+
504
+ // ==================== EXPORTS ====================
505
+
506
+ export default EventListener;