@moneymq/sdk 0.3.2 → 0.7.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.
package/dist/index.mjs CHANGED
@@ -1,4 +1,12 @@
1
1
  // src/catalog.ts
2
+ var PaymentRequiredError = class extends Error {
3
+ constructor(message, paymentRequirements, raw) {
4
+ super(message);
5
+ this.paymentRequirements = paymentRequirements;
6
+ this.raw = raw;
7
+ this.name = "PaymentRequiredError";
8
+ }
9
+ };
2
10
  var ProductsAPI = class {
3
11
  constructor(config) {
4
12
  this.config = config;
@@ -39,7 +47,15 @@ var ProductsAPI = class {
39
47
  if (params?.limit) query.set("limit", String(params.limit));
40
48
  if (params?.startingAfter) query.set("starting_after", params.startingAfter);
41
49
  const queryString = query.toString();
42
- return this.request("GET", `/catalog/v1/products${queryString ? `?${queryString}` : ""}`);
50
+ const result = await this.request(
51
+ "GET",
52
+ `/catalog/v1/products${queryString ? `?${queryString}` : ""}`
53
+ );
54
+ result.data = result.data.map((product) => ({
55
+ ...product,
56
+ accessUrl: `${this.config.endpoint}/catalog/v1/products/${product.id}/access`
57
+ }));
58
+ return result;
43
59
  }
44
60
  /**
45
61
  * Update a product
@@ -53,6 +69,59 @@ var ProductsAPI = class {
53
69
  async delete(id) {
54
70
  return this.request("DELETE", `/catalog/v1/products/${id}`);
55
71
  }
72
+ /**
73
+ * Access a product - gated by x402 payment
74
+ *
75
+ * This endpoint requires payment. If no payment header is provided (or payment is invalid),
76
+ * throws a PaymentRequiredError with the payment requirements.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * try {
81
+ * // First attempt without payment - will throw PaymentRequiredError
82
+ * const access = await moneymq.catalog.products.access('surfnet-max');
83
+ * } catch (error) {
84
+ * if (error instanceof PaymentRequiredError) {
85
+ * // Get payment requirements and create payment
86
+ * const requirements = error.paymentRequirements[0];
87
+ * const paymentHeader = await createPayment(requirements);
88
+ *
89
+ * // Retry with payment
90
+ * const access = await moneymq.catalog.products.access('surfnet-max', {
91
+ * paymentHeader,
92
+ * });
93
+ * }
94
+ * }
95
+ * ```
96
+ */
97
+ async access(id, params) {
98
+ const url = `${this.config.endpoint}/catalog/v1/products/${id}/access`;
99
+ const headers = { "Content-Type": "application/json" };
100
+ if (this.config.secret) {
101
+ headers["Authorization"] = `Bearer ${this.config.secret}`;
102
+ }
103
+ if (params?.paymentHeader) {
104
+ headers["X-Payment"] = params.paymentHeader;
105
+ }
106
+ const response = await fetch(url, {
107
+ method: "GET",
108
+ headers
109
+ });
110
+ if (response.status === 402) {
111
+ const x402Response = await response.json().catch(() => ({}));
112
+ const paymentRequirements = x402Response.accepts || [];
113
+ throw new PaymentRequiredError(
114
+ "Payment required",
115
+ paymentRequirements,
116
+ x402Response
117
+ );
118
+ }
119
+ if (!response.ok) {
120
+ const errorData = await response.json().catch(() => ({}));
121
+ throw new Error(errorData.message || `Request failed: ${response.status}`);
122
+ }
123
+ return response.json();
124
+ }
56
125
  };
57
126
  var PricesAPI = class {
58
127
  constructor(config) {
@@ -256,15 +325,92 @@ var WebhooksAPI = class {
256
325
  return this.request("POST", "/payment/v1/webhooks/test", { event, data });
257
326
  }
258
327
  };
328
+ var PaymentIntentsAPI = class {
329
+ constructor(config) {
330
+ this.request = createRequester(config);
331
+ }
332
+ /**
333
+ * Create a payment intent
334
+ * Use this for simple payments without the full checkout session flow
335
+ */
336
+ async create(params) {
337
+ return this.request("POST", "/catalog/v1/payment_intents", params);
338
+ }
339
+ /**
340
+ * Retrieve a payment intent
341
+ */
342
+ async retrieve(id) {
343
+ return this.request("GET", `/catalog/v1/payment_intents/${id}`);
344
+ }
345
+ /**
346
+ * Confirm a payment intent
347
+ * This triggers the actual payment (and x402 flow if required)
348
+ */
349
+ async confirm(id) {
350
+ return this.request("POST", `/catalog/v1/payment_intents/${id}/confirm`, {});
351
+ }
352
+ /**
353
+ * Cancel a payment intent
354
+ */
355
+ async cancel(id) {
356
+ return this.request("POST", `/catalog/v1/payment_intents/${id}/cancel`, {});
357
+ }
358
+ };
259
359
  var PaymentAPI = class {
260
360
  constructor(config) {
261
361
  this.request = createRequester(config);
262
362
  this.checkout = new CheckoutAPI(config);
363
+ this.intents = new PaymentIntentsAPI(config);
263
364
  this.links = new LinksAPI(config);
264
365
  this.customers = new CustomersAPI(config);
265
366
  this.payouts = new PayoutsAPI(config);
266
367
  this.webhooks = new WebhooksAPI(config);
267
368
  }
369
+ /**
370
+ * Simple one-liner payment - creates a checkout session with inline product data
371
+ *
372
+ * @example
373
+ * ```ts
374
+ * const result = await moneymq.payment.pay({
375
+ * amount: 999,
376
+ * currency: 'usd',
377
+ * productName: 'Pro Plan',
378
+ * productId: 'pro-plan',
379
+ * customer: 'wallet_address',
380
+ * });
381
+ * ```
382
+ */
383
+ async pay(params) {
384
+ const session = await this.request("POST", "/catalog/v1/checkout/sessions", {
385
+ line_items: [
386
+ {
387
+ price_data: {
388
+ currency: params.currency,
389
+ unit_amount: params.amount,
390
+ product_data: {
391
+ name: params.productName,
392
+ description: params.description,
393
+ metadata: {
394
+ product_id: params.productId || params.productName.toLowerCase().replace(/\s+/g, "-")
395
+ }
396
+ }
397
+ },
398
+ quantity: 1
399
+ }
400
+ ],
401
+ customer: params.customer,
402
+ metadata: params.metadata,
403
+ mode: "payment"
404
+ });
405
+ return {
406
+ sessionId: session.id,
407
+ paymentIntentId: session.payment_intent,
408
+ clientSecret: session.client_secret,
409
+ amount: session.amount_total,
410
+ currency: session.currency,
411
+ status: "requires_confirmation"
412
+ };
413
+ }
268
414
  /**
269
415
  * Retrieve a payment by ID
270
416
  */
@@ -285,8 +431,329 @@ var PaymentAPI = class {
285
431
  }
286
432
  };
287
433
 
434
+ // src/config.ts
435
+ async function fetchConfig(apiUrl) {
436
+ const response = await fetch(`${apiUrl}/config`);
437
+ if (!response.ok) {
438
+ throw new Error(`Failed to fetch config: ${response.status}`);
439
+ }
440
+ return response.json();
441
+ }
442
+ async function getRpcUrl(apiUrl, fallback = "https://api.devnet.solana.com") {
443
+ try {
444
+ const config = await fetchConfig(apiUrl);
445
+ return config.x402.validator.rpcUrl || fallback;
446
+ } catch {
447
+ return fallback;
448
+ }
449
+ }
450
+
451
+ // src/x402.ts
452
+ import { createSigner } from "x402-fetch";
453
+ var X402API = class {
454
+ constructor(config) {
455
+ this.config = config;
456
+ this.serverConfig = null;
457
+ this.sandboxAccounts = null;
458
+ }
459
+ /**
460
+ * Fetch sandbox accounts from the server
461
+ */
462
+ async fetchSandboxAccounts() {
463
+ if (this.sandboxAccounts) {
464
+ return this.sandboxAccounts;
465
+ }
466
+ const response = await fetch(`${this.config.endpoint}/sandbox/accounts`);
467
+ if (!response.ok) {
468
+ throw new Error(`Failed to fetch sandbox accounts: ${response.status}`);
469
+ }
470
+ this.sandboxAccounts = await response.json();
471
+ return this.sandboxAccounts;
472
+ }
473
+ /**
474
+ * Get a signer for a sandbox account by tag/label
475
+ *
476
+ * @param params - Parameters containing the wallet tag/label
477
+ * @returns A Signer that can be used directly with wrapFetchWithPayment
478
+ *
479
+ * @example
480
+ * ```typescript
481
+ * const payer = await moneymq.x402.getSigner({ tag: 'alice' });
482
+ * ```
483
+ */
484
+ async getSigner(params) {
485
+ const accounts = await this.fetchSandboxAccounts();
486
+ for (const networkData of Object.values(accounts)) {
487
+ for (const account of networkData.userAccounts) {
488
+ if (account.label === params.tag) {
489
+ if (!account.secretKeyHex) {
490
+ throw new Error(`Account '${params.tag}' does not have a secret key (not locally managed)`);
491
+ }
492
+ return createSigner("solana", account.secretKeyHex);
493
+ }
494
+ }
495
+ }
496
+ throw new Error(`No sandbox account found with label '${params.tag}'`);
497
+ }
498
+ /**
499
+ * Get x402 configuration for use with wrapFetchWithPayment
500
+ *
501
+ * @returns Configuration object compatible with x402-fetch
502
+ *
503
+ * @example
504
+ * ```typescript
505
+ * const config = await moneymq.x402.getConfig();
506
+ * const fetchWithPayment = wrapFetchWithPayment(fetch, payer, undefined, undefined, config);
507
+ * ```
508
+ */
509
+ async getConfig() {
510
+ if (!this.serverConfig) {
511
+ this.serverConfig = await fetchConfig(this.config.endpoint);
512
+ }
513
+ return {
514
+ svmConfig: {
515
+ rpcUrl: this.serverConfig.x402.validator.rpcUrl
516
+ }
517
+ };
518
+ }
519
+ /**
520
+ * Get the full server configuration
521
+ *
522
+ * @returns The complete server configuration including x402 settings
523
+ */
524
+ async getServerConfig() {
525
+ if (!this.serverConfig) {
526
+ this.serverConfig = await fetchConfig(this.config.endpoint);
527
+ }
528
+ return this.serverConfig;
529
+ }
530
+ };
531
+
532
+ // src/events.ts
533
+ var EventStream = class {
534
+ constructor(endpoint, options = {}) {
535
+ this.eventSource = null;
536
+ this.state = "disconnected";
537
+ this.reconnectAttempts = 0;
538
+ this.lastEventId = null;
539
+ this.paymentHandlers = /* @__PURE__ */ new Set();
540
+ this.errorHandlers = /* @__PURE__ */ new Set();
541
+ this.stateHandlers = /* @__PURE__ */ new Set();
542
+ this.endpoint = endpoint;
543
+ console.log("[MoneyMQ SDK] EventStream constructor called with options:", JSON.stringify(options));
544
+ this.options = {
545
+ last: options.last ?? 0,
546
+ cursor: options.cursor ?? "",
547
+ streamId: options.streamId ?? "",
548
+ autoReconnect: options.autoReconnect ?? true,
549
+ reconnectDelay: options.reconnectDelay ?? 1e3,
550
+ maxReconnectAttempts: options.maxReconnectAttempts ?? 0
551
+ };
552
+ console.log("[MoneyMQ SDK] Resolved options:", JSON.stringify(this.options));
553
+ if (this.options.cursor && !this.options.streamId) {
554
+ this.lastEventId = this.options.cursor;
555
+ }
556
+ }
557
+ /**
558
+ * Whether this is a stateful stream (server tracks cursor)
559
+ */
560
+ get isStateful() {
561
+ return this.options.streamId !== "";
562
+ }
563
+ /**
564
+ * The stream ID for stateful streams
565
+ */
566
+ get streamId() {
567
+ return this.options.streamId || null;
568
+ }
569
+ /**
570
+ * Current connection state
571
+ */
572
+ get connectionState() {
573
+ return this.state;
574
+ }
575
+ /**
576
+ * Current cursor (last received event ID)
577
+ */
578
+ get cursor() {
579
+ return this.lastEventId;
580
+ }
581
+ /**
582
+ * Whether the stream is currently connected
583
+ */
584
+ get isConnected() {
585
+ return this.state === "connected";
586
+ }
587
+ on(event, handler) {
588
+ switch (event) {
589
+ case "payment":
590
+ this.paymentHandlers.add(handler);
591
+ return () => this.paymentHandlers.delete(handler);
592
+ case "error":
593
+ this.errorHandlers.add(handler);
594
+ return () => this.errorHandlers.delete(handler);
595
+ case "stateChange":
596
+ this.stateHandlers.add(handler);
597
+ return () => this.stateHandlers.delete(handler);
598
+ }
599
+ }
600
+ off(event, handler) {
601
+ switch (event) {
602
+ case "payment":
603
+ this.paymentHandlers.delete(handler);
604
+ break;
605
+ case "error":
606
+ this.errorHandlers.delete(handler);
607
+ break;
608
+ case "stateChange":
609
+ this.stateHandlers.delete(handler);
610
+ break;
611
+ }
612
+ }
613
+ /**
614
+ * Connect to the event stream
615
+ */
616
+ connect() {
617
+ if (this.eventSource) {
618
+ this.eventSource.close();
619
+ }
620
+ this.setState("connecting");
621
+ const url = this.buildUrl();
622
+ console.log("[MoneyMQ SDK] Connecting to SSE:", url, "| streamId:", this.options.streamId || "(none)");
623
+ this.eventSource = new EventSource(url);
624
+ this.eventSource.onopen = () => {
625
+ this.setState("connected");
626
+ this.reconnectAttempts = 0;
627
+ };
628
+ this.eventSource.addEventListener("payment", (event) => {
629
+ try {
630
+ const cloudEvent = JSON.parse(event.data);
631
+ this.lastEventId = cloudEvent.id;
632
+ this.emitPayment(cloudEvent);
633
+ } catch (e) {
634
+ this.emitError(new Error(`Failed to parse event: ${e}`));
635
+ }
636
+ });
637
+ this.eventSource.onerror = () => {
638
+ this.eventSource?.close();
639
+ this.eventSource = null;
640
+ if (this.options.autoReconnect && this.shouldReconnect()) {
641
+ this.scheduleReconnect();
642
+ } else {
643
+ this.setState("disconnected");
644
+ this.emitError(new Error("Connection lost"));
645
+ }
646
+ };
647
+ }
648
+ /**
649
+ * Disconnect from the event stream
650
+ */
651
+ disconnect() {
652
+ if (this.eventSource) {
653
+ this.eventSource.close();
654
+ this.eventSource = null;
655
+ }
656
+ this.setState("disconnected");
657
+ this.reconnectAttempts = 0;
658
+ }
659
+ /**
660
+ * Reconnect with a new cursor
661
+ * Useful for resuming from a stored position
662
+ */
663
+ reconnectFrom(cursor) {
664
+ this.lastEventId = cursor;
665
+ this.disconnect();
666
+ this.connect();
667
+ }
668
+ buildUrl() {
669
+ const params = new URLSearchParams();
670
+ if (this.options.streamId) {
671
+ params.set("stream_id", this.options.streamId);
672
+ if (this.options.last > 0) {
673
+ params.set("last", this.options.last.toString());
674
+ }
675
+ } else {
676
+ if (this.lastEventId) {
677
+ params.set("cursor", this.lastEventId);
678
+ } else if (this.options.last > 0) {
679
+ params.set("last", this.options.last.toString());
680
+ }
681
+ }
682
+ const queryString = params.toString();
683
+ return queryString ? `${this.endpoint}/events?${queryString}` : `${this.endpoint}/events`;
684
+ }
685
+ setState(state) {
686
+ if (this.state !== state) {
687
+ this.state = state;
688
+ this.stateHandlers.forEach((handler) => handler(state));
689
+ }
690
+ }
691
+ shouldReconnect() {
692
+ if (this.options.maxReconnectAttempts === 0) return true;
693
+ return this.reconnectAttempts < this.options.maxReconnectAttempts;
694
+ }
695
+ scheduleReconnect() {
696
+ this.setState("reconnecting");
697
+ this.reconnectAttempts++;
698
+ setTimeout(() => {
699
+ if (this.state === "reconnecting") {
700
+ this.connect();
701
+ }
702
+ }, this.options.reconnectDelay);
703
+ }
704
+ emitPayment(event) {
705
+ this.paymentHandlers.forEach((handler) => handler(event));
706
+ }
707
+ emitError(error) {
708
+ this.errorHandlers.forEach((handler) => handler(error));
709
+ }
710
+ };
711
+ function createEventStream(endpoint, options) {
712
+ return new EventStream(endpoint, options);
713
+ }
714
+ function isPaymentVerificationSucceeded(event) {
715
+ return event.type === "mq.money.payment.verification.succeeded";
716
+ }
717
+ function isPaymentVerificationFailed(event) {
718
+ return event.type === "mq.money.payment.verification.failed";
719
+ }
720
+ function isPaymentSettlementSucceeded(event) {
721
+ return event.type === "mq.money.payment.settlement.succeeded";
722
+ }
723
+ function isPaymentSettlementFailed(event) {
724
+ return event.type === "mq.money.payment.settlement.failed";
725
+ }
726
+ function parseCloudEvent(data) {
727
+ try {
728
+ return JSON.parse(data);
729
+ } catch {
730
+ return null;
731
+ }
732
+ }
733
+ function buildEventStreamUrl(endpoint, options) {
734
+ const params = new URLSearchParams();
735
+ if (options?.streamId) {
736
+ params.set("stream_id", options.streamId);
737
+ if (options.last && options.last > 0) {
738
+ params.set("last", options.last.toString());
739
+ }
740
+ } else {
741
+ if (options?.cursor) {
742
+ params.set("cursor", options.cursor);
743
+ } else if (options?.last && options.last > 0) {
744
+ params.set("last", options.last.toString());
745
+ }
746
+ }
747
+ const queryString = params.toString();
748
+ return queryString ? `${endpoint}/events?${queryString}` : `${endpoint}/events`;
749
+ }
750
+
288
751
  // src/client.ts
289
752
  var MoneyMQ = class {
753
+ /** MoneyMQ API endpoint */
754
+ get endpoint() {
755
+ return this.config.endpoint;
756
+ }
290
757
  constructor(config) {
291
758
  this.config = {
292
759
  timeout: 3e4,
@@ -294,6 +761,10 @@ var MoneyMQ = class {
294
761
  };
295
762
  this.catalog = new CatalogAPI(this.config);
296
763
  this.payment = new PaymentAPI(this.config);
764
+ this.x402 = new X402API(this.config);
765
+ this.events = {
766
+ stream: (options) => new EventStream(this.config.endpoint, options)
767
+ };
297
768
  }
298
769
  /**
299
770
  * Make an authenticated request to the MoneyMQ API
@@ -331,26 +802,18 @@ var MoneyMQError = class extends Error {
331
802
  this.name = "MoneyMQError";
332
803
  }
333
804
  };
334
-
335
- // src/config.ts
336
- async function fetchConfig(apiUrl) {
337
- const response = await fetch(`${apiUrl}/config`);
338
- if (!response.ok) {
339
- throw new Error(`Failed to fetch config: ${response.status}`);
340
- }
341
- return response.json();
342
- }
343
- async function getRpcUrl(apiUrl, fallback = "https://api.devnet.solana.com") {
344
- try {
345
- const config = await fetchConfig(apiUrl);
346
- return config.x402.validator.rpcUrl || fallback;
347
- } catch {
348
- return fallback;
349
- }
350
- }
351
805
  export {
806
+ EventStream,
352
807
  MoneyMQ,
808
+ PaymentRequiredError,
809
+ buildEventStreamUrl,
810
+ createEventStream,
353
811
  fetchConfig,
354
- getRpcUrl
812
+ getRpcUrl,
813
+ isPaymentSettlementFailed,
814
+ isPaymentSettlementSucceeded,
815
+ isPaymentVerificationFailed,
816
+ isPaymentVerificationSucceeded,
817
+ parseCloudEvent
355
818
  };
356
819
  //# sourceMappingURL=index.mjs.map