moltspay 1.2.1 → 1.4.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 (59) hide show
  1. package/README.md +292 -34
  2. package/dist/cdp/index.d.mts +4 -4
  3. package/dist/cdp/index.d.ts +4 -4
  4. package/dist/cdp/index.js +110 -30368
  5. package/dist/cdp/index.js.map +1 -1
  6. package/dist/cdp/index.mjs +94 -30360
  7. package/dist/cdp/index.mjs.map +1 -1
  8. package/dist/cdp-DeohBe1o.d.ts +66 -0
  9. package/dist/cdp-p_eHuQpb.d.mts +66 -0
  10. package/dist/chains/index.d.mts +9 -8
  11. package/dist/chains/index.d.ts +9 -8
  12. package/dist/chains/index.js +86 -0
  13. package/dist/chains/index.js.map +1 -1
  14. package/dist/chains/index.mjs +86 -0
  15. package/dist/chains/index.mjs.map +1 -1
  16. package/dist/cli/index.js +2746 -290
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/index.mjs +2752 -282
  19. package/dist/cli/index.mjs.map +1 -1
  20. package/dist/client/index.d.mts +60 -4
  21. package/dist/client/index.d.ts +60 -4
  22. package/dist/client/index.js +734 -43
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/index.mjs +732 -41
  25. package/dist/client/index.mjs.map +1 -1
  26. package/dist/facilitators/index.d.mts +220 -39
  27. package/dist/facilitators/index.d.ts +220 -39
  28. package/dist/facilitators/index.js +897 -1
  29. package/dist/facilitators/index.js.map +1 -1
  30. package/dist/facilitators/index.mjs +902 -1
  31. package/dist/facilitators/index.mjs.map +1 -1
  32. package/dist/{index-DgJPZMBG.d.mts → index-D_2FkLwV.d.mts} +6 -2
  33. package/dist/{index-DgJPZMBG.d.ts → index-D_2FkLwV.d.ts} +6 -2
  34. package/dist/index.d.mts +3 -2
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.js +2238 -30837
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.mjs +2167 -30766
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/server/index.d.mts +30 -3
  41. package/dist/server/index.d.ts +30 -3
  42. package/dist/server/index.js +1345 -54
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +1355 -54
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/verify/index.d.mts +1 -1
  47. package/dist/verify/index.d.ts +1 -1
  48. package/dist/verify/index.js +86 -0
  49. package/dist/verify/index.js.map +1 -1
  50. package/dist/verify/index.mjs +86 -0
  51. package/dist/verify/index.mjs.map +1 -1
  52. package/dist/wallet/index.d.mts +3 -3
  53. package/dist/wallet/index.d.ts +3 -3
  54. package/dist/wallet/index.js +86 -0
  55. package/dist/wallet/index.js.map +1 -1
  56. package/dist/wallet/index.mjs +86 -0
  57. package/dist/wallet/index.mjs.map +1 -1
  58. package/package.json +8 -2
  59. package/schemas/moltspay.services.schema.json +27 -132
@@ -30,10 +30,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/facilitators/index.ts
31
31
  var facilitators_exports = {};
32
32
  __export(facilitators_exports, {
33
+ BNBFacilitator: () => BNBFacilitator,
33
34
  BaseFacilitator: () => BaseFacilitator,
34
35
  CDPFacilitator: () => CDPFacilitator,
35
36
  FacilitatorRegistry: () => FacilitatorRegistry,
37
+ SolanaFacilitator: () => SolanaFacilitator,
38
+ TempoFacilitator: () => TempoFacilitator,
39
+ createIntentTypedData: () => createIntentTypedData,
36
40
  createRegistry: () => createRegistry,
41
+ createSolanaPaymentTransaction: () => createSolanaPaymentTransaction,
37
42
  getDefaultRegistry: () => getDefaultRegistry
38
43
  });
39
44
  module.exports = __toCommonJS(facilitators_exports);
@@ -264,7 +269,879 @@ var CDPFacilitator = class extends BaseFacilitator {
264
269
  }
265
270
  };
266
271
 
272
+ // src/chains/index.ts
273
+ var CHAINS = {
274
+ // ============ Mainnet ============
275
+ base: {
276
+ name: "Base",
277
+ chainId: 8453,
278
+ rpc: "https://mainnet.base.org",
279
+ tokens: {
280
+ USDC: {
281
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
282
+ decimals: 6,
283
+ symbol: "USDC",
284
+ eip712Name: "USD Coin"
285
+ // EIP-712 domain name
286
+ },
287
+ USDT: {
288
+ address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
289
+ decimals: 6,
290
+ symbol: "USDT",
291
+ eip712Name: "Tether USD"
292
+ }
293
+ },
294
+ usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
295
+ // deprecated, for backward compat
296
+ explorer: "https://basescan.org/address/",
297
+ explorerTx: "https://basescan.org/tx/",
298
+ avgBlockTime: 2
299
+ },
300
+ polygon: {
301
+ name: "Polygon",
302
+ chainId: 137,
303
+ rpc: "https://polygon-bor-rpc.publicnode.com",
304
+ tokens: {
305
+ USDC: {
306
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
307
+ decimals: 6,
308
+ symbol: "USDC",
309
+ eip712Name: "USD Coin"
310
+ },
311
+ USDT: {
312
+ address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
313
+ decimals: 6,
314
+ symbol: "USDT",
315
+ eip712Name: "(PoS) Tether USD"
316
+ // Polygon uses this name
317
+ }
318
+ },
319
+ usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
320
+ explorer: "https://polygonscan.com/address/",
321
+ explorerTx: "https://polygonscan.com/tx/",
322
+ avgBlockTime: 2
323
+ },
324
+ // ============ Testnet ============
325
+ base_sepolia: {
326
+ name: "Base Sepolia",
327
+ chainId: 84532,
328
+ rpc: "https://sepolia.base.org",
329
+ tokens: {
330
+ USDC: {
331
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
332
+ decimals: 6,
333
+ symbol: "USDC",
334
+ eip712Name: "USDC"
335
+ // Testnet USDC uses 'USDC' not 'USD Coin'
336
+ },
337
+ USDT: {
338
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
339
+ // Same as USDC on testnet (no official USDT)
340
+ decimals: 6,
341
+ symbol: "USDT",
342
+ eip712Name: "USDC"
343
+ // Uses same contract as USDC
344
+ }
345
+ },
346
+ usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
347
+ explorer: "https://sepolia.basescan.org/address/",
348
+ explorerTx: "https://sepolia.basescan.org/tx/",
349
+ avgBlockTime: 2
350
+ },
351
+ // ============ Tempo Testnet (Moderato) ============
352
+ tempo_moderato: {
353
+ name: "Tempo Moderato",
354
+ chainId: 42431,
355
+ rpc: "https://rpc.moderato.tempo.xyz",
356
+ tokens: {
357
+ // TIP-20 stablecoins on Tempo testnet (from mppx SDK)
358
+ // Note: Tempo uses USD as native gas token, not ETH
359
+ USDC: {
360
+ address: "0x20c0000000000000000000000000000000000000",
361
+ // pathUSD - primary testnet stablecoin
362
+ decimals: 6,
363
+ symbol: "USDC",
364
+ eip712Name: "pathUSD"
365
+ },
366
+ USDT: {
367
+ address: "0x20c0000000000000000000000000000000000001",
368
+ // alphaUSD
369
+ decimals: 6,
370
+ symbol: "USDT",
371
+ eip712Name: "alphaUSD"
372
+ }
373
+ },
374
+ usdc: "0x20c0000000000000000000000000000000000000",
375
+ explorer: "https://explore.testnet.tempo.xyz/address/",
376
+ explorerTx: "https://explore.testnet.tempo.xyz/tx/",
377
+ avgBlockTime: 0.5
378
+ // ~500ms finality
379
+ },
380
+ // ============ BNB Chain Testnet ============
381
+ bnb_testnet: {
382
+ name: "BNB Testnet",
383
+ chainId: 97,
384
+ rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
385
+ tokens: {
386
+ // Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
387
+ // Using official Binance-Peg testnet tokens
388
+ USDC: {
389
+ address: "0x64544969ed7EBf5f083679233325356EbE738930",
390
+ // Testnet USDC
391
+ decimals: 18,
392
+ symbol: "USDC",
393
+ eip712Name: "USD Coin"
394
+ },
395
+ USDT: {
396
+ address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
397
+ // Testnet USDT
398
+ decimals: 18,
399
+ symbol: "USDT",
400
+ eip712Name: "Tether USD"
401
+ }
402
+ },
403
+ usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
404
+ explorer: "https://testnet.bscscan.com/address/",
405
+ explorerTx: "https://testnet.bscscan.com/tx/",
406
+ avgBlockTime: 3,
407
+ // BNB-specific: requires approval for pay-for-success flow
408
+ requiresApproval: true
409
+ },
410
+ // ============ BNB Chain Mainnet ============
411
+ bnb: {
412
+ name: "BNB Smart Chain",
413
+ chainId: 56,
414
+ rpc: "https://bsc-dataseed.binance.org",
415
+ tokens: {
416
+ // Note: BNB uses 18 decimals for stablecoins
417
+ USDC: {
418
+ address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
419
+ decimals: 18,
420
+ symbol: "USDC",
421
+ eip712Name: "USD Coin"
422
+ },
423
+ USDT: {
424
+ address: "0x55d398326f99059fF775485246999027B3197955",
425
+ decimals: 18,
426
+ symbol: "USDT",
427
+ eip712Name: "Tether USD"
428
+ }
429
+ },
430
+ usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
431
+ explorer: "https://bscscan.com/address/",
432
+ explorerTx: "https://bscscan.com/tx/",
433
+ avgBlockTime: 3,
434
+ // BNB-specific: requires approval for pay-for-success flow
435
+ requiresApproval: true
436
+ }
437
+ };
438
+
439
+ // src/facilitators/tempo.ts
440
+ var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
441
+ var TempoFacilitator = class extends BaseFacilitator {
442
+ name = "tempo";
443
+ displayName = "Tempo Testnet";
444
+ supportedNetworks = ["eip155:42431"];
445
+ // Tempo Moderato
446
+ rpcUrl;
447
+ constructor() {
448
+ super();
449
+ this.rpcUrl = CHAINS.tempo_moderato.rpc;
450
+ }
451
+ async healthCheck() {
452
+ const start = Date.now();
453
+ try {
454
+ const response = await fetch(this.rpcUrl, {
455
+ method: "POST",
456
+ headers: { "Content-Type": "application/json" },
457
+ body: JSON.stringify({
458
+ jsonrpc: "2.0",
459
+ method: "eth_chainId",
460
+ params: [],
461
+ id: 1
462
+ })
463
+ });
464
+ const data = await response.json();
465
+ const chainId = parseInt(data.result, 16);
466
+ if (chainId !== 42431) {
467
+ return { healthy: false, error: `Wrong chainId: ${chainId}` };
468
+ }
469
+ return { healthy: true, latencyMs: Date.now() - start };
470
+ } catch (error) {
471
+ return { healthy: false, error: String(error) };
472
+ }
473
+ }
474
+ async verify(paymentPayload, requirements) {
475
+ try {
476
+ const tempoPayload = paymentPayload.payload;
477
+ if (!tempoPayload?.txHash) {
478
+ return { valid: false, error: "Missing txHash in payment payload" };
479
+ }
480
+ const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
481
+ if (!receipt) {
482
+ return { valid: false, error: "Transaction not found" };
483
+ }
484
+ if (receipt.status !== "0x1") {
485
+ return { valid: false, error: "Transaction failed" };
486
+ }
487
+ const transferLog = receipt.logs.find(
488
+ (log) => log.topics[0] === TRANSFER_EVENT_TOPIC
489
+ );
490
+ if (!transferLog) {
491
+ return { valid: false, error: "No Transfer event found" };
492
+ }
493
+ const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
494
+ const expectedTo = requirements.payTo.toLowerCase();
495
+ if (toAddress !== expectedTo) {
496
+ return {
497
+ valid: false,
498
+ error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
499
+ };
500
+ }
501
+ const amount = BigInt(transferLog.data);
502
+ const expectedAmount = BigInt(requirements.amount);
503
+ if (amount < expectedAmount) {
504
+ return {
505
+ valid: false,
506
+ error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
507
+ };
508
+ }
509
+ const tokenAddress = transferLog.address.toLowerCase();
510
+ const expectedToken = requirements.asset.toLowerCase();
511
+ if (tokenAddress !== expectedToken) {
512
+ return {
513
+ valid: false,
514
+ error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
515
+ };
516
+ }
517
+ return {
518
+ valid: true,
519
+ details: {
520
+ txHash: tempoPayload.txHash,
521
+ from: "0x" + transferLog.topics[1].slice(26),
522
+ to: toAddress,
523
+ amount: amount.toString(),
524
+ token: tokenAddress
525
+ }
526
+ };
527
+ } catch (error) {
528
+ return { valid: false, error: `Verification failed: ${error}` };
529
+ }
530
+ }
531
+ async settle(paymentPayload, requirements) {
532
+ const verifyResult = await this.verify(paymentPayload, requirements);
533
+ if (!verifyResult.valid) {
534
+ return { success: false, error: verifyResult.error };
535
+ }
536
+ const tempoPayload = paymentPayload.payload;
537
+ return {
538
+ success: true,
539
+ transaction: tempoPayload.txHash,
540
+ status: "settled"
541
+ };
542
+ }
543
+ async getTransactionReceipt(txHash) {
544
+ const response = await fetch(this.rpcUrl, {
545
+ method: "POST",
546
+ headers: { "Content-Type": "application/json" },
547
+ body: JSON.stringify({
548
+ jsonrpc: "2.0",
549
+ method: "eth_getTransactionReceipt",
550
+ params: [txHash],
551
+ id: 1
552
+ })
553
+ });
554
+ const data = await response.json();
555
+ return data.result;
556
+ }
557
+ };
558
+
559
+ // src/facilitators/bnb.ts
560
+ var import_accounts = require("viem/accounts");
561
+ var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
562
+ var EIP712_DOMAIN = {
563
+ name: "MoltsPay",
564
+ version: "1"
565
+ };
566
+ var INTENT_TYPES = {
567
+ PaymentIntent: [
568
+ { name: "from", type: "address" },
569
+ { name: "to", type: "address" },
570
+ { name: "amount", type: "uint256" },
571
+ { name: "token", type: "address" },
572
+ { name: "service", type: "string" },
573
+ { name: "nonce", type: "uint256" },
574
+ { name: "deadline", type: "uint256" }
575
+ ]
576
+ };
577
+ var BNBFacilitator = class extends BaseFacilitator {
578
+ name = "bnb";
579
+ displayName = "BNB Smart Chain";
580
+ supportedNetworks = ["eip155:56", "eip155:97"];
581
+ // Mainnet + Testnet
582
+ serverPrivateKey;
583
+ spenderAddress = null;
584
+ chainConfigs;
585
+ constructor(serverPrivateKey) {
586
+ super();
587
+ this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
588
+ if (this.serverPrivateKey) {
589
+ const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
590
+ const account = (0, import_accounts.privateKeyToAccount)(key);
591
+ this.spenderAddress = account.address;
592
+ }
593
+ this.chainConfigs = {
594
+ 56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
595
+ 97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
596
+ };
597
+ }
598
+ async healthCheck() {
599
+ const start = Date.now();
600
+ try {
601
+ const response = await fetch(this.chainConfigs[56].rpc, {
602
+ method: "POST",
603
+ headers: { "Content-Type": "application/json" },
604
+ body: JSON.stringify({
605
+ jsonrpc: "2.0",
606
+ method: "eth_chainId",
607
+ params: [],
608
+ id: 1
609
+ })
610
+ });
611
+ const data = await response.json();
612
+ const chainId = parseInt(data.result, 16);
613
+ if (chainId !== 56) {
614
+ return { healthy: false, error: `Wrong chainId: ${chainId}` };
615
+ }
616
+ return { healthy: true, latencyMs: Date.now() - start };
617
+ } catch (error) {
618
+ return { healthy: false, error: String(error) };
619
+ }
620
+ }
621
+ /**
622
+ * Verify a payment intent signature (before service execution)
623
+ *
624
+ * This verifies:
625
+ * 1. Signature is valid for the intent
626
+ * 2. Client has approved server wallet
627
+ * 3. Client has sufficient balance
628
+ * 4. Intent hasn't expired
629
+ */
630
+ async verify(paymentPayload, requirements) {
631
+ try {
632
+ const bnbPayload = paymentPayload.payload;
633
+ if (!bnbPayload?.intent) {
634
+ return { valid: false, error: "Missing intent in payment payload" };
635
+ }
636
+ const { intent, chainId } = bnbPayload;
637
+ const config = this.chainConfigs[chainId];
638
+ if (!config) {
639
+ return { valid: false, error: `Unsupported chainId: ${chainId}` };
640
+ }
641
+ if (intent.deadline < Date.now()) {
642
+ return { valid: false, error: "Intent expired" };
643
+ }
644
+ const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
645
+ if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
646
+ return { valid: false, error: "Invalid signature" };
647
+ }
648
+ if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
649
+ return { valid: false, error: `Wrong recipient: ${intent.to}` };
650
+ }
651
+ if (BigInt(intent.amount) < BigInt(requirements.amount)) {
652
+ return { valid: false, error: `Insufficient amount: ${intent.amount}` };
653
+ }
654
+ if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
655
+ return { valid: false, error: `Wrong token: ${intent.token}` };
656
+ }
657
+ const serverAddress = await this.getServerAddress();
658
+ const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
659
+ if (BigInt(allowance) < BigInt(intent.amount)) {
660
+ return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
661
+ }
662
+ const balance = await this.getBalance(intent.from, intent.token, config.rpc);
663
+ if (BigInt(balance) < BigInt(intent.amount)) {
664
+ return { valid: false, error: "Insufficient balance" };
665
+ }
666
+ return {
667
+ valid: true,
668
+ details: {
669
+ from: intent.from,
670
+ to: intent.to,
671
+ amount: intent.amount,
672
+ token: intent.token,
673
+ service: intent.service,
674
+ nonce: intent.nonce,
675
+ deadline: intent.deadline
676
+ }
677
+ };
678
+ } catch (error) {
679
+ return { valid: false, error: `Verification failed: ${error}` };
680
+ }
681
+ }
682
+ /**
683
+ * Settle a payment by executing transferFrom
684
+ *
685
+ * This is called AFTER the service has been successfully delivered.
686
+ * Server pays gas, transfers tokens from client to provider.
687
+ */
688
+ async settle(paymentPayload, requirements) {
689
+ if (!this.serverPrivateKey) {
690
+ return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
691
+ }
692
+ try {
693
+ const verifyResult = await this.verify(paymentPayload, requirements);
694
+ if (!verifyResult.valid) {
695
+ return { success: false, error: verifyResult.error };
696
+ }
697
+ const bnbPayload = paymentPayload.payload;
698
+ const { intent, chainId } = bnbPayload;
699
+ const config = this.chainConfigs[chainId];
700
+ const txHash = await this.executeTransferFrom(
701
+ intent.from,
702
+ intent.to,
703
+ intent.amount,
704
+ intent.token,
705
+ config.rpc
706
+ );
707
+ return {
708
+ success: true,
709
+ transaction: txHash,
710
+ status: "settled"
711
+ };
712
+ } catch (error) {
713
+ return { success: false, error: `Settlement failed: ${error}` };
714
+ }
715
+ }
716
+ /**
717
+ * Check if client has approved the server wallet
718
+ */
719
+ async checkApproval(clientAddress, token, chainId) {
720
+ const config = this.chainConfigs[chainId];
721
+ if (!config) {
722
+ throw new Error(`Unsupported chainId: ${chainId}`);
723
+ }
724
+ const serverAddress = await this.getServerAddress();
725
+ const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
726
+ const minAllowance = BigInt("1000000000000000000000");
727
+ return {
728
+ approved: BigInt(allowance) >= minAllowance,
729
+ allowance
730
+ };
731
+ }
732
+ /**
733
+ * Verify a completed transaction (for checking past payments)
734
+ */
735
+ async verifyTransaction(txHash, expected, chainId) {
736
+ const config = this.chainConfigs[chainId];
737
+ if (!config) {
738
+ return { valid: false, error: `Unsupported chainId: ${chainId}` };
739
+ }
740
+ try {
741
+ const receipt = await this.getTransactionReceipt(txHash, config.rpc);
742
+ if (!receipt) {
743
+ return { valid: false, error: "Transaction not found" };
744
+ }
745
+ if (receipt.status !== "0x1") {
746
+ return { valid: false, error: "Transaction failed" };
747
+ }
748
+ const transferLog = receipt.logs.find(
749
+ (log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
750
+ );
751
+ if (!transferLog) {
752
+ return { valid: false, error: "No Transfer event found" };
753
+ }
754
+ const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
755
+ if (toAddress !== expected.to.toLowerCase()) {
756
+ return { valid: false, error: `Wrong recipient: ${toAddress}` };
757
+ }
758
+ const amount = BigInt(transferLog.data);
759
+ if (amount < BigInt(expected.amount)) {
760
+ return { valid: false, error: `Insufficient amount: ${amount}` };
761
+ }
762
+ return {
763
+ valid: true,
764
+ details: {
765
+ txHash,
766
+ from: "0x" + transferLog.topics[1].slice(26),
767
+ to: toAddress,
768
+ amount: amount.toString(),
769
+ token: transferLog.address
770
+ }
771
+ };
772
+ } catch (error) {
773
+ return { valid: false, error: `Verification failed: ${error}` };
774
+ }
775
+ }
776
+ // ==================== Private Methods ====================
777
+ /**
778
+ * Get the server's spender address (public, for 402 responses)
779
+ * Returns cached value computed at construction time.
780
+ */
781
+ getSpenderAddress() {
782
+ return this.spenderAddress;
783
+ }
784
+ async getServerAddress() {
785
+ const { ethers } = await import("ethers");
786
+ const wallet = new ethers.Wallet(this.serverPrivateKey);
787
+ return wallet.address;
788
+ }
789
+ async recoverIntentSigner(intent, chainId) {
790
+ const { ethers } = await import("ethers");
791
+ const domain = {
792
+ ...EIP712_DOMAIN,
793
+ chainId
794
+ };
795
+ const message = {
796
+ from: intent.from,
797
+ to: intent.to,
798
+ amount: intent.amount,
799
+ token: intent.token,
800
+ service: intent.service,
801
+ nonce: intent.nonce,
802
+ deadline: intent.deadline
803
+ };
804
+ const recoveredAddress = ethers.verifyTypedData(
805
+ domain,
806
+ INTENT_TYPES,
807
+ message,
808
+ intent.signature
809
+ );
810
+ return recoveredAddress;
811
+ }
812
+ async getAllowance(owner, spender, token, rpcUrl) {
813
+ const selector = "0xdd62ed3e";
814
+ const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
815
+ const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
816
+ const data = selector + ownerPadded + spenderPadded;
817
+ const response = await fetch(rpcUrl, {
818
+ method: "POST",
819
+ headers: { "Content-Type": "application/json" },
820
+ body: JSON.stringify({
821
+ jsonrpc: "2.0",
822
+ method: "eth_call",
823
+ params: [{ to: token, data }, "latest"],
824
+ id: 1
825
+ })
826
+ });
827
+ const result = await response.json();
828
+ return result.result || "0x0";
829
+ }
830
+ async getBalance(account, token, rpcUrl) {
831
+ const selector = "0x70a08231";
832
+ const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
833
+ const data = selector + accountPadded;
834
+ const response = await fetch(rpcUrl, {
835
+ method: "POST",
836
+ headers: { "Content-Type": "application/json" },
837
+ body: JSON.stringify({
838
+ jsonrpc: "2.0",
839
+ method: "eth_call",
840
+ params: [{ to: token, data }, "latest"],
841
+ id: 1
842
+ })
843
+ });
844
+ const result = await response.json();
845
+ return result.result || "0x0";
846
+ }
847
+ async executeTransferFrom(from, to, amount, token, rpcUrl) {
848
+ const { ethers } = await import("ethers");
849
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
850
+ const wallet = new ethers.Wallet(this.serverPrivateKey, provider);
851
+ const tokenContract = new ethers.Contract(token, [
852
+ "function transferFrom(address from, address to, uint256 amount) returns (bool)"
853
+ ], wallet);
854
+ const tx = await tokenContract.transferFrom(from, to, amount);
855
+ const receipt = await tx.wait();
856
+ return receipt.hash;
857
+ }
858
+ async getTransactionReceipt(txHash, rpcUrl) {
859
+ const response = await fetch(rpcUrl, {
860
+ method: "POST",
861
+ headers: { "Content-Type": "application/json" },
862
+ body: JSON.stringify({
863
+ jsonrpc: "2.0",
864
+ method: "eth_getTransactionReceipt",
865
+ params: [txHash],
866
+ id: 1
867
+ })
868
+ });
869
+ const data = await response.json();
870
+ return data.result;
871
+ }
872
+ };
873
+ function createIntentTypedData(intent, chainId) {
874
+ return {
875
+ domain: {
876
+ ...EIP712_DOMAIN,
877
+ chainId
878
+ },
879
+ types: INTENT_TYPES,
880
+ primaryType: "PaymentIntent",
881
+ message: {
882
+ from: intent.from,
883
+ to: intent.to,
884
+ amount: intent.amount,
885
+ token: intent.token,
886
+ service: intent.service,
887
+ nonce: intent.nonce,
888
+ deadline: intent.deadline
889
+ }
890
+ };
891
+ }
892
+
893
+ // src/facilitators/solana.ts
894
+ var import_web32 = require("@solana/web3.js");
895
+ var import_spl_token = require("@solana/spl-token");
896
+
897
+ // src/chains/solana.ts
898
+ var import_web3 = require("@solana/web3.js");
899
+ var SOLANA_CHAINS = {
900
+ solana: {
901
+ name: "Solana Mainnet",
902
+ cluster: "mainnet-beta",
903
+ rpc: "https://api.mainnet-beta.solana.com",
904
+ explorer: "https://solscan.io/account/",
905
+ explorerTx: "https://solscan.io/tx/",
906
+ tokens: {
907
+ USDC: {
908
+ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
909
+ // Circle official USDC
910
+ decimals: 6
911
+ }
912
+ }
913
+ },
914
+ solana_devnet: {
915
+ name: "Solana Devnet",
916
+ cluster: "devnet",
917
+ rpc: "https://api.devnet.solana.com",
918
+ explorer: "https://solscan.io/account/",
919
+ explorerTx: "https://solscan.io/tx/",
920
+ tokens: {
921
+ USDC: {
922
+ // Circle's devnet USDC (if not available, we'll deploy our own test token)
923
+ mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
924
+ decimals: 6
925
+ }
926
+ }
927
+ }
928
+ };
929
+
930
+ // src/facilitators/solana.ts
931
+ var SolanaFacilitator = class extends BaseFacilitator {
932
+ name = "solana";
933
+ displayName = "Solana Direct";
934
+ supportedNetworks = ["solana:mainnet", "solana:devnet"];
935
+ connections = /* @__PURE__ */ new Map();
936
+ feePayerKeypair;
937
+ constructor(config) {
938
+ super();
939
+ this.feePayerKeypair = config?.feePayerKeypair;
940
+ for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
941
+ this.connections.set(
942
+ chain,
943
+ new import_web32.Connection(config2.rpc, "confirmed")
944
+ );
945
+ }
946
+ if (this.feePayerKeypair) {
947
+ console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
948
+ }
949
+ }
950
+ /**
951
+ * Get fee payer public key (for gasless transactions)
952
+ */
953
+ getFeePayerPubkey() {
954
+ return this.feePayerKeypair?.publicKey.toBase58() || null;
955
+ }
956
+ getConnection(chain) {
957
+ const conn = this.connections.get(chain);
958
+ if (!conn) {
959
+ throw new Error(`No connection for chain: ${chain}`);
960
+ }
961
+ return conn;
962
+ }
963
+ /**
964
+ * Convert our chain name to network identifier
965
+ */
966
+ static chainToNetwork(chain) {
967
+ return chain === "solana" ? "solana:mainnet" : "solana:devnet";
968
+ }
969
+ /**
970
+ * Convert network identifier to chain name
971
+ */
972
+ static networkToChain(network) {
973
+ if (network === "solana:mainnet") return "solana";
974
+ if (network === "solana:devnet") return "solana_devnet";
975
+ return null;
976
+ }
977
+ async healthCheck() {
978
+ const start = Date.now();
979
+ try {
980
+ const conn = this.getConnection("solana_devnet");
981
+ await conn.getSlot();
982
+ return {
983
+ healthy: true,
984
+ latencyMs: Date.now() - start
985
+ };
986
+ } catch (error) {
987
+ return {
988
+ healthy: false,
989
+ error: error.message
990
+ };
991
+ }
992
+ }
993
+ /**
994
+ * Verify a Solana payment
995
+ *
996
+ * Checks:
997
+ * 1. Transaction is valid and properly signed
998
+ * 2. Transfer instruction matches expected amount and recipient
999
+ */
1000
+ async verify(paymentPayload, requirements) {
1001
+ try {
1002
+ const solanaPayload = paymentPayload.payload;
1003
+ if (!solanaPayload || !solanaPayload.signedTransaction) {
1004
+ return { valid: false, error: "Missing signed transaction" };
1005
+ }
1006
+ const chain = solanaPayload.chain || "solana_devnet";
1007
+ const chainConfig = SOLANA_CHAINS[chain];
1008
+ if (!chainConfig) {
1009
+ return { valid: false, error: `Invalid chain: ${chain}` };
1010
+ }
1011
+ const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
1012
+ let tx;
1013
+ try {
1014
+ tx = import_web32.Transaction.from(txBuffer);
1015
+ } catch {
1016
+ tx = import_web32.VersionedTransaction.deserialize(txBuffer);
1017
+ }
1018
+ if (tx instanceof import_web32.Transaction) {
1019
+ const hasAnySignature = tx.signatures.some(
1020
+ (sig) => sig.signature && !sig.signature.every((b) => b === 0)
1021
+ );
1022
+ if (!hasAnySignature) {
1023
+ return { valid: false, error: "Transaction not signed" };
1024
+ }
1025
+ }
1026
+ const expectedAmount = BigInt(requirements.amount);
1027
+ const expectedRecipient = new import_web32.PublicKey(requirements.payTo);
1028
+ return {
1029
+ valid: true,
1030
+ details: {
1031
+ chain,
1032
+ sender: solanaPayload.sender,
1033
+ recipient: requirements.payTo,
1034
+ amount: requirements.amount
1035
+ }
1036
+ };
1037
+ } catch (error) {
1038
+ return { valid: false, error: error.message };
1039
+ }
1040
+ }
1041
+ /**
1042
+ * Settle a Solana payment
1043
+ *
1044
+ * Submits the signed transaction to the network.
1045
+ * In gasless mode, adds fee payer signature before submitting.
1046
+ */
1047
+ async settle(paymentPayload, requirements) {
1048
+ try {
1049
+ const solanaPayload = paymentPayload.payload;
1050
+ if (!solanaPayload || !solanaPayload.signedTransaction) {
1051
+ return { success: false, error: "Missing signed transaction" };
1052
+ }
1053
+ const chain = solanaPayload.chain || "solana_devnet";
1054
+ const connection = this.getConnection(chain);
1055
+ const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
1056
+ let txToSend;
1057
+ try {
1058
+ const tx = import_web32.Transaction.from(txBuffer);
1059
+ if (this.feePayerKeypair && tx.feePayer) {
1060
+ const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
1061
+ const txFeePayer = tx.feePayer.toBase58();
1062
+ if (txFeePayer === feePayerPubkey) {
1063
+ console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
1064
+ tx.partialSign(this.feePayerKeypair);
1065
+ }
1066
+ }
1067
+ txToSend = tx.serialize();
1068
+ } catch (e) {
1069
+ txToSend = txBuffer;
1070
+ }
1071
+ const signature = await connection.sendRawTransaction(txToSend, {
1072
+ skipPreflight: false,
1073
+ preflightCommitment: "confirmed"
1074
+ });
1075
+ const confirmation = await connection.confirmTransaction(signature, "confirmed");
1076
+ if (confirmation.value.err) {
1077
+ return {
1078
+ success: false,
1079
+ error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
1080
+ transaction: signature
1081
+ };
1082
+ }
1083
+ return {
1084
+ success: true,
1085
+ transaction: signature,
1086
+ status: "confirmed"
1087
+ };
1088
+ } catch (error) {
1089
+ return { success: false, error: error.message };
1090
+ }
1091
+ }
1092
+ supportsNetwork(network) {
1093
+ return this.supportedNetworks.includes(network);
1094
+ }
1095
+ };
1096
+ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
1097
+ const chainConfig = SOLANA_CHAINS[chain];
1098
+ const connection = new import_web32.Connection(chainConfig.rpc, "confirmed");
1099
+ const mint = new import_web32.PublicKey(chainConfig.tokens.USDC.mint);
1100
+ const actualFeePayer = feePayerPubkey || senderPubkey;
1101
+ const senderATA = await (0, import_spl_token.getAssociatedTokenAddress)(mint, senderPubkey);
1102
+ const recipientATA = await (0, import_spl_token.getAssociatedTokenAddress)(mint, recipientPubkey);
1103
+ const transaction = new import_web32.Transaction();
1104
+ try {
1105
+ await (0, import_spl_token.getAccount)(connection, recipientATA);
1106
+ } catch {
1107
+ transaction.add(
1108
+ (0, import_spl_token.createAssociatedTokenAccountInstruction)(
1109
+ actualFeePayer,
1110
+ // payer (fee payer in gasless mode)
1111
+ recipientATA,
1112
+ // ata to create
1113
+ recipientPubkey,
1114
+ // owner
1115
+ mint
1116
+ // mint
1117
+ )
1118
+ );
1119
+ }
1120
+ transaction.add(
1121
+ (0, import_spl_token.createTransferCheckedInstruction)(
1122
+ senderATA,
1123
+ // source
1124
+ mint,
1125
+ // mint
1126
+ recipientATA,
1127
+ // destination
1128
+ senderPubkey,
1129
+ // owner (sender still authorizes the transfer)
1130
+ amount,
1131
+ // amount
1132
+ chainConfig.tokens.USDC.decimals
1133
+ // decimals
1134
+ )
1135
+ );
1136
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
1137
+ transaction.recentBlockhash = blockhash;
1138
+ transaction.feePayer = actualFeePayer;
1139
+ return transaction;
1140
+ }
1141
+
267
1142
  // src/facilitators/registry.ts
1143
+ var import_web33 = require("@solana/web3.js");
1144
+ var import_bs58 = __toESM(require("bs58"));
268
1145
  var FacilitatorRegistry = class {
269
1146
  factories = /* @__PURE__ */ new Map();
270
1147
  instances = /* @__PURE__ */ new Map();
@@ -272,7 +1149,21 @@ var FacilitatorRegistry = class {
272
1149
  roundRobinIndex = 0;
273
1150
  constructor(selection) {
274
1151
  this.registerFactory("cdp", (config) => new CDPFacilitator(config));
275
- this.selection = selection || { primary: "cdp", strategy: "failover" };
1152
+ this.registerFactory("tempo", () => new TempoFacilitator());
1153
+ this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
1154
+ this.registerFactory("solana", (config) => {
1155
+ let feePayerKeypair;
1156
+ const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
1157
+ if (feePayerKey) {
1158
+ try {
1159
+ feePayerKeypair = import_web33.Keypair.fromSecretKey(import_bs58.default.decode(feePayerKey));
1160
+ } catch (e) {
1161
+ console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
1162
+ }
1163
+ }
1164
+ return new SolanaFacilitator({ feePayerKeypair });
1165
+ });
1166
+ this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
276
1167
  }
277
1168
  /**
278
1169
  * Register a new facilitator factory
@@ -499,10 +1390,15 @@ function createRegistry(selection) {
499
1390
  }
500
1391
  // Annotate the CommonJS export names for ESM import in node:
501
1392
  0 && (module.exports = {
1393
+ BNBFacilitator,
502
1394
  BaseFacilitator,
503
1395
  CDPFacilitator,
504
1396
  FacilitatorRegistry,
1397
+ SolanaFacilitator,
1398
+ TempoFacilitator,
1399
+ createIntentTypedData,
505
1400
  createRegistry,
1401
+ createSolanaPaymentTransaction,
506
1402
  getDefaultRegistry
507
1403
  });
508
1404
  //# sourceMappingURL=index.js.map