moltspay 1.5.0 → 2.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 (55) hide show
  1. package/README.md +143 -0
  2. package/dist/cdp/index.js.map +1 -1
  3. package/dist/cdp/index.mjs.map +1 -1
  4. package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
  5. package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
  6. package/dist/chains/index.d.mts +37 -1
  7. package/dist/chains/index.d.ts +37 -1
  8. package/dist/chains/index.js +22 -0
  9. package/dist/chains/index.js.map +1 -1
  10. package/dist/chains/index.mjs +19 -0
  11. package/dist/chains/index.mjs.map +1 -1
  12. package/dist/cli/index.js +1941 -204
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/index.mjs +1938 -201
  15. package/dist/cli/index.mjs.map +1 -1
  16. package/dist/client/index.d.mts +46 -0
  17. package/dist/client/index.d.ts +46 -0
  18. package/dist/client/index.js +1059 -118
  19. package/dist/client/index.js.map +1 -1
  20. package/dist/client/index.mjs +1055 -116
  21. package/dist/client/index.mjs.map +1 -1
  22. package/dist/client/web/index.d.mts +434 -0
  23. package/dist/client/web/index.mjs +1300 -0
  24. package/dist/client/web/index.mjs.map +1 -0
  25. package/dist/facilitators/index.d.mts +185 -6
  26. package/dist/facilitators/index.d.ts +185 -6
  27. package/dist/facilitators/index.js +497 -13
  28. package/dist/facilitators/index.js.map +1 -1
  29. package/dist/facilitators/index.mjs +492 -13
  30. package/dist/facilitators/index.mjs.map +1 -1
  31. package/dist/index.d.mts +77 -2
  32. package/dist/index.d.ts +77 -2
  33. package/dist/index.js +1865 -172
  34. package/dist/index.js.map +1 -1
  35. package/dist/index.mjs +1854 -167
  36. package/dist/index.mjs.map +1 -1
  37. package/dist/mcp/index.js +1062 -123
  38. package/dist/mcp/index.js.map +1 -1
  39. package/dist/mcp/index.mjs +1059 -120
  40. package/dist/mcp/index.mjs.map +1 -1
  41. package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
  42. package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
  43. package/dist/server/index.d.mts +137 -2
  44. package/dist/server/index.d.ts +137 -2
  45. package/dist/server/index.js +757 -30
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/server/index.mjs +757 -30
  48. package/dist/server/index.mjs.map +1 -1
  49. package/dist/verify/index.js.map +1 -1
  50. package/dist/verify/index.mjs.map +1 -1
  51. package/dist/wallet/index.js.map +1 -1
  52. package/dist/wallet/index.mjs.map +1 -1
  53. package/package.json +15 -2
  54. package/schemas/moltspay.services.schema.json +100 -6
  55. package/scripts/postinstall.js +91 -0
@@ -13,15 +13,15 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
13
13
 
14
14
  // src/mcp/server.ts
15
15
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
16
- import { join as join3 } from "path";
16
+ import { join as join4 } from "path";
17
17
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
18
  import { z } from "zod";
19
19
 
20
- // src/client/index.ts
20
+ // src/client/node/index.ts
21
21
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
22
- import { homedir as homedir2 } from "os";
23
- import { join as join2 } from "path";
24
- import { Wallet, ethers } from "ethers";
22
+ import { homedir as homedir3 } from "os";
23
+ import { join as join3 } from "path";
24
+ import { Wallet as Wallet2, ethers as ethers2 } from "ethers";
25
25
 
26
26
  // src/chains/index.ts
27
27
  var CHAINS = {
@@ -272,16 +272,16 @@ import {
272
272
  getAccount as getAccount2,
273
273
  createAssociatedTokenAccountInstruction
274
274
  } from "@solana/spl-token";
275
- async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
275
+ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey, connection) {
276
276
  const chainConfig = SOLANA_CHAINS[chain];
277
- const connection = new Connection3(chainConfig.rpc, "confirmed");
277
+ const conn = connection ?? new Connection3(chainConfig.rpc, "confirmed");
278
278
  const mint = new PublicKey3(chainConfig.tokens.USDC.mint);
279
279
  const actualFeePayer = feePayerPubkey || senderPubkey;
280
280
  const senderATA = await getAssociatedTokenAddress2(mint, senderPubkey);
281
281
  const recipientATA = await getAssociatedTokenAddress2(mint, recipientPubkey);
282
282
  const transaction = new Transaction();
283
283
  try {
284
- await getAccount2(connection, recipientATA);
284
+ await getAccount2(conn, recipientATA);
285
285
  } catch {
286
286
  transaction.add(
287
287
  createAssociatedTokenAccountInstruction(
@@ -312,17 +312,915 @@ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amo
312
312
  // decimals
313
313
  )
314
314
  );
315
- const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
315
+ const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash();
316
316
  transaction.recentBlockhash = blockhash;
317
317
  transaction.feePayer = actualFeePayer;
318
318
  return transaction;
319
319
  }
320
320
 
321
- // src/client/index.ts
321
+ // src/client/node/index.ts
322
322
  import { PublicKey as PublicKey4 } from "@solana/web3.js";
323
+
324
+ // src/client/core/types.ts
323
325
  var X402_VERSION = 2;
324
326
  var PAYMENT_REQUIRED_HEADER = "x-payment-required";
325
327
  var PAYMENT_HEADER = "x-payment";
328
+
329
+ // src/client/core/chain-map.ts
330
+ var NETWORK_TO_CHAIN = {
331
+ "eip155:8453": "base",
332
+ "eip155:137": "polygon",
333
+ "eip155:84532": "base_sepolia",
334
+ "eip155:42431": "tempo_moderato",
335
+ "eip155:56": "bnb",
336
+ "eip155:97": "bnb_testnet",
337
+ "solana:mainnet": "solana",
338
+ "solana:devnet": "solana_devnet"
339
+ };
340
+ var CHAIN_TO_NETWORK = Object.fromEntries(
341
+ Object.entries(NETWORK_TO_CHAIN).map(([network, chain]) => [chain, network])
342
+ );
343
+ function networkToChainName(network) {
344
+ return NETWORK_TO_CHAIN[network] ?? null;
345
+ }
346
+
347
+ // src/client/core/base64.ts
348
+ var BufferCtor = globalThis.Buffer;
349
+
350
+ // src/client/core/errors.ts
351
+ var MoltsPayError = class extends Error {
352
+ code;
353
+ constructor(code, message) {
354
+ super(message);
355
+ this.name = "MoltsPayError";
356
+ this.code = code;
357
+ }
358
+ };
359
+
360
+ // src/client/core/eip3009.ts
361
+ var EIP3009_TYPES = {
362
+ TransferWithAuthorization: [
363
+ { name: "from", type: "address" },
364
+ { name: "to", type: "address" },
365
+ { name: "value", type: "uint256" },
366
+ { name: "validAfter", type: "uint256" },
367
+ { name: "validBefore", type: "uint256" },
368
+ { name: "nonce", type: "bytes32" }
369
+ ]
370
+ };
371
+ function buildEIP3009TypedData(args) {
372
+ const validAfter = args.validAfter ?? "0";
373
+ const validBefore = args.validBefore ?? (Math.floor(Date.now() / 1e3) + 3600).toString();
374
+ const authorization = {
375
+ from: args.from,
376
+ to: args.to,
377
+ value: args.value,
378
+ validAfter,
379
+ validBefore,
380
+ nonce: args.nonce
381
+ };
382
+ return {
383
+ domain: {
384
+ name: args.tokenName,
385
+ version: args.tokenVersion,
386
+ chainId: args.chainId,
387
+ verifyingContract: args.tokenAddress
388
+ },
389
+ types: EIP3009_TYPES,
390
+ primaryType: "TransferWithAuthorization",
391
+ message: authorization
392
+ };
393
+ }
394
+
395
+ // src/client/core/bnb-intent.ts
396
+ var BNB_INTENT_TYPES = {
397
+ PaymentIntent: [
398
+ { name: "from", type: "address" },
399
+ { name: "to", type: "address" },
400
+ { name: "amount", type: "uint256" },
401
+ { name: "token", type: "address" },
402
+ { name: "service", type: "string" },
403
+ { name: "nonce", type: "uint256" },
404
+ { name: "deadline", type: "uint256" }
405
+ ]
406
+ };
407
+ var BNB_DOMAIN_NAME = "MoltsPay";
408
+ var BNB_DOMAIN_VERSION = "1";
409
+ function buildBnbIntentTypedData(args) {
410
+ const intent = {
411
+ from: args.from,
412
+ to: args.to,
413
+ amount: args.amount,
414
+ token: args.tokenAddress,
415
+ service: args.service,
416
+ nonce: args.nonce,
417
+ deadline: args.deadline
418
+ };
419
+ return {
420
+ domain: {
421
+ name: BNB_DOMAIN_NAME,
422
+ version: BNB_DOMAIN_VERSION,
423
+ chainId: args.chainId
424
+ },
425
+ types: BNB_INTENT_TYPES,
426
+ primaryType: "PaymentIntent",
427
+ message: intent
428
+ };
429
+ }
430
+
431
+ // src/client/node/signer.ts
432
+ import { ethers } from "ethers";
433
+ import { Transaction as Transaction2 } from "@solana/web3.js";
434
+ var NodeSigner = class {
435
+ evmWallet;
436
+ getSolanaKeypair;
437
+ constructor(evmWallet, options = {}) {
438
+ this.evmWallet = evmWallet;
439
+ this.getSolanaKeypair = options.getSolanaKeypair ?? (() => null);
440
+ }
441
+ async getEvmAddress() {
442
+ return this.evmWallet.address;
443
+ }
444
+ async getSolanaAddress() {
445
+ const kp = this.getSolanaKeypair();
446
+ return kp ? kp.publicKey.toBase58() : null;
447
+ }
448
+ async signTypedData(envelope) {
449
+ const mutableTypes = {};
450
+ for (const [key, fields] of Object.entries(envelope.types)) {
451
+ mutableTypes[key] = [...fields];
452
+ }
453
+ return this.evmWallet.signTypedData(
454
+ envelope.domain,
455
+ mutableTypes,
456
+ envelope.message
457
+ );
458
+ }
459
+ async sendEvmTransaction(args) {
460
+ const chain = findChainByChainId(args.chainId);
461
+ if (!chain) {
462
+ throw new Error(`sendEvmTransaction: unknown chainId ${args.chainId}`);
463
+ }
464
+ const provider = new ethers.JsonRpcProvider(chain.rpc);
465
+ const connected = this.evmWallet.connect(provider);
466
+ const tx = await connected.sendTransaction({
467
+ to: args.to,
468
+ data: args.data,
469
+ value: args.value ? BigInt(args.value) : 0n
470
+ });
471
+ return tx.hash;
472
+ }
473
+ async signSolanaTransaction(args) {
474
+ const kp = this.getSolanaKeypair();
475
+ if (!kp) {
476
+ throw new Error("signSolanaTransaction: no Solana wallet configured");
477
+ }
478
+ const tx = Transaction2.from(Buffer.from(args.transactionBase64, "base64"));
479
+ if (args.partialSign) {
480
+ tx.partialSign(kp);
481
+ } else {
482
+ tx.sign(kp);
483
+ }
484
+ return tx.serialize({ requireAllSignatures: false }).toString("base64");
485
+ }
486
+ };
487
+ function findChainByChainId(chainId) {
488
+ for (const cfg of Object.values(CHAINS)) {
489
+ if (cfg.chainId === chainId) {
490
+ return cfg;
491
+ }
492
+ }
493
+ return void 0;
494
+ }
495
+
496
+ // src/client/alipay/index.ts
497
+ import { randomUUID } from "crypto";
498
+ import { mkdir, writeFile } from "fs/promises";
499
+ import { homedir as homedir2 } from "os";
500
+ import { join as join2 } from "path";
501
+
502
+ // src/client/alipay/cli.ts
503
+ import { spawn } from "child_process";
504
+
505
+ // src/client/alipay/log.ts
506
+ var RANK = { off: 0, info: 1, debug: 2 };
507
+ function normalizeLevel(v) {
508
+ const s = (v ?? "").toLowerCase();
509
+ return s === "info" || s === "debug" ? s : "off";
510
+ }
511
+ var level = normalizeLevel(process.env.MOLTSPAY_ALIPAY_LOG);
512
+ var sink = (line) => {
513
+ process.stderr.write(line + "\n");
514
+ };
515
+ function enabledFor(want) {
516
+ return RANK[level] >= RANK[want];
517
+ }
518
+ function render(event, fields) {
519
+ const parts = ["[moltspay:alipay]", String(fields.ts), event];
520
+ if (fields.flow) parts.push(`flow=${fields.flow}`);
521
+ if (fields.step) parts.push(`step=${fields.step}`);
522
+ if (typeof fields.ms === "number") parts.push(`${fields.ms}ms`);
523
+ for (const [k, v] of Object.entries(fields)) {
524
+ if (["ts", "level", "event", "flow", "step", "ms"].includes(k)) continue;
525
+ parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
526
+ }
527
+ return parts.join(" ");
528
+ }
529
+ function emit(lvl, event, fields) {
530
+ if (!enabledFor(lvl)) return;
531
+ const full = { ts: (/* @__PURE__ */ new Date()).toISOString(), level: lvl, event, ...fields };
532
+ try {
533
+ sink(render(event, full), full);
534
+ } catch {
535
+ }
536
+ }
537
+ var alipayLog = {
538
+ info: (event, fields = {}) => emit("info", event, fields),
539
+ debug: (event, fields = {}) => emit("debug", event, fields),
540
+ /** True if anything at all would be logged (cheap guard for hot paths). */
541
+ enabled: () => level !== "off"
542
+ };
543
+ async function timeStep(step, flow, fn) {
544
+ if (!alipayLog.enabled()) return fn();
545
+ const t0 = Date.now();
546
+ alipayLog.debug("step.start", { flow, step });
547
+ try {
548
+ const out = await fn();
549
+ alipayLog.info("step.end", { flow, step, ms: Date.now() - t0, ok: true });
550
+ return out;
551
+ } catch (err2) {
552
+ alipayLog.info("step.end", {
553
+ flow,
554
+ step,
555
+ ms: Date.now() - t0,
556
+ ok: false,
557
+ err: err2 instanceof Error ? err2.message : String(err2)
558
+ });
559
+ throw err2;
560
+ }
561
+ }
562
+
563
+ // src/client/alipay/cli.ts
564
+ var ALLOWED_ENV = /* @__PURE__ */ new Set([
565
+ "AIPAY_OUTPUT_CHANNEL",
566
+ "AIPAY_SESSION_ID",
567
+ "AIPAY_FRAMEWORK",
568
+ "AIPAY_MODEL",
569
+ "AIPAY_OS",
570
+ // Minimal survival set for spawn to find the binary and a home dir.
571
+ "PATH",
572
+ "HOME"
573
+ ]);
574
+ function filterEnv(env) {
575
+ return Object.fromEntries(
576
+ Object.entries(env).filter(([k]) => ALLOWED_ENV.has(k))
577
+ );
578
+ }
579
+ function makeLineSplitter(onLine) {
580
+ let buf = "";
581
+ return (chunk) => {
582
+ buf += chunk.toString("utf-8");
583
+ let nl;
584
+ while ((nl = buf.indexOf("\n")) !== -1) {
585
+ onLine(buf.slice(0, nl));
586
+ buf = buf.slice(nl + 1);
587
+ }
588
+ };
589
+ }
590
+ function profileEnv(step, flow) {
591
+ const want = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE;
592
+ const hook = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_HOOK;
593
+ if (!want || !hook) return {};
594
+ const steps = want.split(",").map((s) => s.trim());
595
+ if (!steps.includes("all") && !steps.includes(step)) return {};
596
+ const dir = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_DIR || "/tmp";
597
+ const safe = `${flow ?? "noflow"}-${step}-${Date.now()}`.replace(/[^a-zA-Z0-9._-]/g, "_");
598
+ const out = `${dir}/cli-profile-${safe}.json`;
599
+ alipayLog.info("cli.profile", { flow, step, out });
600
+ return {
601
+ NODE_OPTIONS: `--require=${hook}`,
602
+ MOLTSPAY_CLI_PROFILE_OUT: out
603
+ };
604
+ }
605
+ var runCli = (args, opts = {}) => {
606
+ const bin = opts.bin ?? "alipay-bot";
607
+ const step = opts.step ?? args[0] ?? "(no-arg)";
608
+ const startedAt = Date.now();
609
+ const lines = [];
610
+ const collect = (line) => {
611
+ lines.push(line);
612
+ alipayLog.debug("cli.line", {
613
+ flow: opts.flow,
614
+ step,
615
+ ms: Date.now() - startedAt,
616
+ preview: line.slice(0, 120)
617
+ });
618
+ opts.onLine?.(line);
619
+ };
620
+ alipayLog.debug("step.start", { flow: opts.flow, step });
621
+ return new Promise((resolve, reject) => {
622
+ const child = spawn(bin, args, {
623
+ env: { ...filterEnv(process.env), ...profileEnv(step, opts.flow), ...opts.env ?? {} }
624
+ });
625
+ if (opts.signal) {
626
+ if (opts.signal.aborted) child.kill("SIGTERM");
627
+ opts.signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
628
+ }
629
+ let sawByte = false;
630
+ const onChunk = (stream) => (chunk) => {
631
+ const ms = Date.now() - startedAt;
632
+ if (!sawByte) {
633
+ sawByte = true;
634
+ alipayLog.debug("cli.firstbyte", { flow: opts.flow, step, ms });
635
+ }
636
+ alipayLog.debug("cli.chunk", { flow: opts.flow, step, ms, stream, bytes: chunk.length });
637
+ };
638
+ const onStdout = makeLineSplitter(collect);
639
+ const onStderr = makeLineSplitter(collect);
640
+ child.stdout?.on("data", onChunk("out"));
641
+ child.stderr?.on("data", onChunk("err"));
642
+ child.stdout?.on("data", onStdout);
643
+ child.stderr?.on("data", onStderr);
644
+ child.on("error", (err2) => {
645
+ alipayLog.info("cli.exit", {
646
+ flow: opts.flow,
647
+ step,
648
+ ms: Date.now() - startedAt,
649
+ ok: false,
650
+ err: err2.message
651
+ });
652
+ reject(err2);
653
+ });
654
+ child.on("close", (code) => {
655
+ alipayLog.info("cli.exit", {
656
+ flow: opts.flow,
657
+ step,
658
+ ms: Date.now() - startedAt,
659
+ ok: (code ?? 1) === 0,
660
+ code: code ?? 1
661
+ });
662
+ resolve({ exitCode: code ?? 1, lines });
663
+ });
664
+ });
665
+ };
666
+
667
+ // src/client/alipay/install.ts
668
+ import { execFile } from "child_process";
669
+ import { promisify } from "util";
670
+
671
+ // src/client/alipay/errors.ts
672
+ var AlipayCliNotFoundError = class extends MoltsPayError {
673
+ constructor(message) {
674
+ super("ALIPAY_CLI_NOT_FOUND", message);
675
+ this.name = "AlipayCliNotFoundError";
676
+ }
677
+ };
678
+ var AlipayCliVersionError = class extends MoltsPayError {
679
+ constructor(message) {
680
+ super("ALIPAY_CLI_VERSION", message);
681
+ this.name = "AlipayCliVersionError";
682
+ }
683
+ };
684
+ var NeedsWalletSetupError = class extends MoltsPayError {
685
+ constructor(message = "Alipay wallet not set up. Run: moltspay alipay apply") {
686
+ super("ALIPAY_NEEDS_WALLET_SETUP", message);
687
+ this.name = "NeedsWalletSetupError";
688
+ }
689
+ };
690
+ var AlipayPaymentRejectedError = class extends MoltsPayError {
691
+ constructor(message) {
692
+ super("ALIPAY_PAYMENT_REJECTED", message);
693
+ this.name = "AlipayPaymentRejectedError";
694
+ }
695
+ };
696
+ var AlipayPaymentTimeoutError = class extends MoltsPayError {
697
+ constructor(message) {
698
+ super("ALIPAY_PAYMENT_TIMEOUT", message);
699
+ this.name = "AlipayPaymentTimeoutError";
700
+ }
701
+ };
702
+ var AlipayProtocolError = class extends MoltsPayError {
703
+ constructor(message) {
704
+ super("ALIPAY_PROTOCOL", message);
705
+ this.name = "AlipayProtocolError";
706
+ }
707
+ };
708
+ var UnsupportedRailError = class extends MoltsPayError {
709
+ constructor(rail, message) {
710
+ super("UNSUPPORTED_RAIL", message ?? `Rail not supported by server: ${rail}`);
711
+ this.rail = rail;
712
+ this.name = "UnsupportedRailError";
713
+ }
714
+ };
715
+
716
+ // src/client/alipay/install.ts
717
+ var execFileAsync = promisify(execFile);
718
+ var MIN_CLI_VERSION = "0.3.15";
719
+ function semverLt(a, b) {
720
+ const pa = a.split(".").map((n) => parseInt(n, 10));
721
+ const pb = b.split(".").map((n) => parseInt(n, 10));
722
+ for (let i = 0; i < 3; i++) {
723
+ const x = pa[i] ?? 0;
724
+ const y = pb[i] ?? 0;
725
+ if (x !== y) return x < y;
726
+ }
727
+ return false;
728
+ }
729
+ function parseVersion(stdout) {
730
+ return stdout.match(/v?(\d+\.\d+\.\d+)/)?.[1] ?? null;
731
+ }
732
+ var defaultGetVersion = async () => {
733
+ const { stdout } = await execFileAsync("alipay-bot", ["--version"]);
734
+ return stdout;
735
+ };
736
+ async function ensureCliUncached(getVersion) {
737
+ let stdout;
738
+ try {
739
+ stdout = await getVersion();
740
+ } catch (e) {
741
+ if (e?.code === "ENOENT") {
742
+ throw new AlipayCliNotFoundError(
743
+ "alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
744
+ );
745
+ }
746
+ throw e;
747
+ }
748
+ const version = parseVersion(stdout);
749
+ if (!version || semverLt(version, MIN_CLI_VERSION)) {
750
+ throw new AlipayCliVersionError(
751
+ `alipay-bot ${version ?? "?"} found, need \u2265 ${MIN_CLI_VERSION}. Run: npx -y @alipay/agent-payment@latest update`
752
+ );
753
+ }
754
+ return version;
755
+ }
756
+ var cachedVersion = null;
757
+ var inflight = null;
758
+ async function ensureCli(getVersion = defaultGetVersion) {
759
+ if (getVersion !== defaultGetVersion) {
760
+ return ensureCliUncached(getVersion);
761
+ }
762
+ if (cachedVersion) return cachedVersion;
763
+ if (!inflight) {
764
+ inflight = ensureCliUncached(getVersion).then((v) => {
765
+ cachedVersion = v;
766
+ return v;
767
+ }).finally(() => {
768
+ inflight = null;
769
+ });
770
+ }
771
+ return inflight;
772
+ }
773
+
774
+ // src/client/alipay/poll.ts
775
+ var POLL_INTERVAL_MS = 3e3;
776
+ var POLL_MAX_INFLIGHT = 2;
777
+ function resolveMaxInflight(override) {
778
+ if (override !== void 0) return Math.max(1, Math.floor(override));
779
+ const raw = process.env.MOLTSPAY_ALIPAY_POLL_MAX_INFLIGHT;
780
+ if (raw !== void 0 && raw.trim() !== "") {
781
+ const n = Number(raw);
782
+ if (Number.isFinite(n) && n >= 1) return Math.floor(n);
783
+ }
784
+ return POLL_MAX_INFLIGHT;
785
+ }
786
+ function resolveLaunchGap(override) {
787
+ if (override !== void 0) return Math.max(0, override);
788
+ const raw = process.env.MOLTSPAY_ALIPAY_POLL_LAUNCH_MS;
789
+ if (raw !== void 0 && raw.trim() !== "") {
790
+ const n = Number(raw);
791
+ if (Number.isFinite(n) && n >= 0) return n;
792
+ }
793
+ return POLL_INTERVAL_MS;
794
+ }
795
+ function parseStatus(lines) {
796
+ const raw = lines.join("\n").trim();
797
+ try {
798
+ const json = JSON.parse(raw);
799
+ if (json && typeof json === "object") {
800
+ if (typeof json.success === "boolean") {
801
+ if (json.success) return "paid";
802
+ const err2 = String(json.errorCode ?? "").toUpperCase();
803
+ if (/UNPAID|WAIT|PENDING|PROCESS|NOTPAY/.test(err2)) return "pending";
804
+ if (/CLOSED|CANCEL|FAIL|REJECT|REFUSE|TIMEOUT|EXPIRE/.test(err2)) return "rejected";
805
+ return "pending";
806
+ }
807
+ if (typeof json.code !== "undefined") {
808
+ if (Number(json.code) === 200) return "paid";
809
+ const msg = `${json.message ?? ""}${json.reason ?? ""}`;
810
+ if (/关闭|失败|拒绝|取消|超时|已撤销/.test(msg)) return "rejected";
811
+ return "pending";
812
+ }
813
+ if (typeof json.body === "string") {
814
+ return classifyReportText(json.body);
815
+ }
816
+ }
817
+ } catch {
818
+ }
819
+ return classifyReportText(raw);
820
+ }
821
+ function classifyReportText(s) {
822
+ const text = s.toUpperCase();
823
+ if (/查询支付状态成功并获取资源/.test(s) || /资源响应状态[^0-9\n]{0,8}200/.test(s) || /TRADE_SUCCESS|TRADE_FINISHED|"STATUS":\s*"FULFILLED"/.test(text)) return "paid";
824
+ if (/TRADE_CLOSED|REJECTED|REFUSE|CANCEL/.test(text) || /交易关闭|支付失败|已拒绝|已取消|已撤销|交易超时/.test(s)) return "rejected";
825
+ if (/WAIT_BUYER_PAY|UNPAID|PENDING|WAITING/.test(text) || /交易未支付|等待付款|待支付|未支付/.test(s)) return "pending";
826
+ return "unknown";
827
+ }
828
+ function defaultSleep(ms, signal) {
829
+ return new Promise((resolve, reject) => {
830
+ if (signal?.aborted) return reject(new Error("aborted"));
831
+ const t = setTimeout(resolve, ms);
832
+ signal?.addEventListener(
833
+ "abort",
834
+ () => {
835
+ clearTimeout(t);
836
+ reject(new Error("aborted"));
837
+ },
838
+ { once: true }
839
+ );
840
+ });
841
+ }
842
+ var LAUNCH = /* @__PURE__ */ Symbol("launch");
843
+ async function pollUntil(tradeNo, resourceUrl, opts) {
844
+ const runner = opts.runner ?? runCli;
845
+ const sleep = opts.sleep ?? defaultSleep;
846
+ const now = opts.now ?? Date.now;
847
+ const maxInflight = resolveMaxInflight(opts.maxInflight);
848
+ const launchGap = resolveLaunchGap(opts.launchIntervalMs);
849
+ const args = [
850
+ "402-query-payment-status",
851
+ "-t",
852
+ tradeNo,
853
+ "-r",
854
+ resourceUrl,
855
+ ...opts.method ? ["-m", opts.method] : [],
856
+ ...opts.data ? ["-d", opts.data] : []
857
+ ];
858
+ const internal = new AbortController();
859
+ const signal = opts.signal ? AbortSignal.any([opts.signal, internal.signal]) : internal.signal;
860
+ let tick = 0;
861
+ let lastLaunch = -Infinity;
862
+ const inflight2 = /* @__PURE__ */ new Set();
863
+ const launch = () => {
864
+ tick += 1;
865
+ const myTick = tick;
866
+ lastLaunch = now();
867
+ const run = (async () => {
868
+ const { lines } = await runner(args, {
869
+ signal,
870
+ step: "query-payment-status",
871
+ flow: tradeNo
872
+ });
873
+ const status = parseStatus(lines);
874
+ alipayLog.debug("poll.tick", { flow: tradeNo, tick: myTick, status });
875
+ return { status, lines };
876
+ })();
877
+ const tracked = run.finally(() => {
878
+ inflight2.delete(tracked);
879
+ });
880
+ inflight2.add(tracked);
881
+ };
882
+ try {
883
+ for (; ; ) {
884
+ if (opts.signal?.aborted) throw new Error("aborted");
885
+ const expired = now() >= opts.deadline;
886
+ if (expired && inflight2.size === 0) {
887
+ throw new AlipayPaymentTimeoutError(
888
+ `Payment ${tradeNo} not completed before pay_before deadline`
889
+ );
890
+ }
891
+ while (!expired && inflight2.size < maxInflight && now() - lastLaunch >= launchGap) {
892
+ launch();
893
+ }
894
+ const waiters = [...inflight2];
895
+ if (!expired && inflight2.size < maxInflight) {
896
+ const untilNext = Math.max(0, launchGap - (now() - lastLaunch));
897
+ waiters.push(sleep(untilNext, signal).then(() => LAUNCH, () => LAUNCH));
898
+ }
899
+ const winner = await Promise.race(waiters);
900
+ if (winner === LAUNCH) continue;
901
+ if (winner.status === "paid") return { status: "paid", lines: winner.lines };
902
+ if (winner.status === "rejected") {
903
+ throw new AlipayPaymentRejectedError(`Payment ${tradeNo} was rejected`);
904
+ }
905
+ }
906
+ } finally {
907
+ internal.abort();
908
+ for (const p of inflight2) p.catch(() => void 0);
909
+ }
910
+ }
911
+
912
+ // src/client/alipay/index.ts
913
+ var TRADE_NO_RE = /^\d{32}$/;
914
+ function resolveSessionId(explicit, envSession) {
915
+ return explicit ?? envSession ?? randomUUID();
916
+ }
917
+ function assertTradeNo(t) {
918
+ if (!TRADE_NO_RE.test(t)) {
919
+ throw new AlipayProtocolError(`invalid tradeNo (expect 32 digits): ${t}`);
920
+ }
921
+ }
922
+ function parseTradeNo(lines) {
923
+ for (const line of lines) {
924
+ const labeled = line.match(/trade[_-]?no["'\s:=]+(\d{32})/i);
925
+ if (labeled) return labeled[1];
926
+ const bare = line.match(/\b(\d{32})\b/);
927
+ if (bare) return bare[1];
928
+ }
929
+ return null;
930
+ }
931
+ function parsePaymentUrl(lines) {
932
+ let paymentUrl;
933
+ let shortenUrl;
934
+ for (const line of lines) {
935
+ const m = line.match(/(alipays?:\/\/\S+|https?:\/\/\S+)/i);
936
+ if (!m) continue;
937
+ const url = m[1].replace(/[)\]`>]+$/, "");
938
+ if (/short|qr\.alipay|surl|\/s\//i.test(line) && !shortenUrl) shortenUrl = url;
939
+ else if (!paymentUrl) paymentUrl = url;
940
+ }
941
+ if (!paymentUrl && shortenUrl) paymentUrl = shortenUrl;
942
+ return { paymentUrl, shortenUrl };
943
+ }
944
+ function extractMedia(line) {
945
+ const m = line.match(/^\s*MEDIA:\s*(.+?)\s*$/);
946
+ return m ? m[1] : null;
947
+ }
948
+ function isWalletReady(lines) {
949
+ const text = lines.join("\n").trim();
950
+ try {
951
+ const json = JSON.parse(text);
952
+ if (json && typeof json.code !== "undefined") return Number(json.code) === 200;
953
+ } catch {
954
+ }
955
+ return !/未开通|未开启|NOT[_\s-]*(OPEN|BOUND|SET)|NEEDS?[_\s-]*SETUP|NO[_\s-]*WALLET/i.test(text);
956
+ }
957
+ function extractResourceFromReport(report) {
958
+ const label = report.search(/资源响应体/);
959
+ const start = report.indexOf("{", label === -1 ? 0 : label);
960
+ if (start === -1) return void 0;
961
+ let depth = 0, inStr = false, esc = false;
962
+ for (let i = start; i < report.length; i++) {
963
+ const ch = report[i];
964
+ if (inStr) {
965
+ if (esc) esc = false;
966
+ else if (ch === "\\") esc = true;
967
+ else if (ch === '"') inStr = false;
968
+ continue;
969
+ }
970
+ if (ch === '"') inStr = true;
971
+ else if (ch === "{") depth++;
972
+ else if (ch === "}" && --depth === 0) {
973
+ const slice = report.slice(start, i + 1);
974
+ try {
975
+ const obj = JSON.parse(slice);
976
+ const r = obj?.resourceResponse ?? obj?.result ?? obj?.data ?? obj?.body ?? obj;
977
+ return typeof r === "string" ? r : JSON.stringify(r);
978
+ } catch {
979
+ return slice;
980
+ }
981
+ }
982
+ }
983
+ return void 0;
984
+ }
985
+ function extractBody(lines) {
986
+ const text = lines.join("\n").trim();
987
+ try {
988
+ const json = JSON.parse(text);
989
+ if (json && typeof json === "object") {
990
+ if (json.resourceResponse !== void 0 && json.resourceResponse !== null) {
991
+ const rr = json.resourceResponse;
992
+ const r = typeof rr === "object" ? rr.result ?? rr.data ?? rr.body ?? rr : rr;
993
+ return typeof r === "string" ? r : JSON.stringify(r);
994
+ }
995
+ if (typeof json.body === "string") {
996
+ const inner = extractResourceFromReport(json.body);
997
+ return inner ?? json.body;
998
+ }
999
+ const body = json.data ?? json.result ?? json.body ?? json.resource;
1000
+ if (body !== void 0) return typeof body === "string" ? body : JSON.stringify(body);
1001
+ return text;
1002
+ }
1003
+ } catch {
1004
+ }
1005
+ const idx = lines.findIndex((l) => /^\s*BODY:/.test(l));
1006
+ if (idx !== -1) {
1007
+ const first = lines[idx].replace(/^\s*BODY:\s*/, "");
1008
+ return [first, ...lines.slice(idx + 1)].join("\n").trim();
1009
+ }
1010
+ return lines.filter((l) => !/^\s*(MEDIA|STATUS|INFO):/.test(l)).join("\n").trim();
1011
+ }
1012
+ var DEFAULT_WALLET_TTL_MS = 10 * 60 * 1e3;
1013
+ function resolveWalletTtlMs() {
1014
+ const raw = process.env.MOLTSPAY_ALIPAY_WALLET_TTL_MS;
1015
+ if (raw === void 0 || raw.trim() === "") return DEFAULT_WALLET_TTL_MS;
1016
+ const n = Number(raw);
1017
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_WALLET_TTL_MS;
1018
+ }
1019
+ var walletReadyUntil = /* @__PURE__ */ new Map();
1020
+ var DEFAULT_INTENT_TTL_MS = 10 * 60 * 1e3;
1021
+ function resolveIntentTtlMs() {
1022
+ const raw = process.env.MOLTSPAY_ALIPAY_INTENT_TTL_MS;
1023
+ if (raw === void 0 || raw.trim() === "") return DEFAULT_INTENT_TTL_MS;
1024
+ const n = Number(raw);
1025
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTENT_TTL_MS;
1026
+ }
1027
+ var intentDoneUntil = /* @__PURE__ */ new Map();
1028
+ var AlipayClient = class {
1029
+ sessionId;
1030
+ configDir;
1031
+ framework;
1032
+ runner;
1033
+ getVersion;
1034
+ now;
1035
+ /** Only the default runner may use the process-level wallet cache. */
1036
+ walletCacheable;
1037
+ /** Only the default runner may use the process-level payment-intent cache. */
1038
+ intentCacheable;
1039
+ constructor(opts = {}) {
1040
+ this.sessionId = resolveSessionId(opts.sessionId, process.env.AIPAY_SESSION_ID);
1041
+ this.configDir = opts.configDir ?? join2(homedir2(), ".moltspay");
1042
+ this.framework = opts.framework ?? process.env.AIPAY_FRAMEWORK ?? "openclaw";
1043
+ this.runner = opts.runner ?? runCli;
1044
+ this.getVersion = opts.getVersion;
1045
+ this.now = opts.now ?? Date.now;
1046
+ this.walletCacheable = opts.cacheWallet ?? !opts.runner;
1047
+ this.intentCacheable = opts.cacheIntent ?? !opts.runner;
1048
+ }
1049
+ /**
1050
+ * Throws NeedsWalletSetupError unless alipay-bot reports an opened wallet.
1051
+ *
1052
+ * Skips the ~22s `check-wallet` spawn entirely when a prior call cached a
1053
+ * "ready" verdict within the TTL (see {@link DEFAULT_WALLET_TTL_MS}).
1054
+ */
1055
+ async checkWallet(signal, flow) {
1056
+ const ttlMs = resolveWalletTtlMs();
1057
+ const cacheKey = `${this.configDir}::${this.framework}`;
1058
+ const useCache = this.walletCacheable && ttlMs > 0;
1059
+ if (useCache) {
1060
+ const until = walletReadyUntil.get(cacheKey);
1061
+ if (until !== void 0 && this.now() < until) {
1062
+ alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: true, ttlMsLeft: until - this.now() });
1063
+ return;
1064
+ }
1065
+ }
1066
+ const { lines } = await this.runner(["check-wallet"], { signal, step: "check-wallet", flow });
1067
+ if (!isWalletReady(lines)) {
1068
+ if (this.walletCacheable) walletReadyUntil.delete(cacheKey);
1069
+ throw new NeedsWalletSetupError(
1070
+ "Alipay wallet not opened. Run: moltspay alipay apply (then: moltspay alipay bind)"
1071
+ );
1072
+ }
1073
+ if (useCache) {
1074
+ walletReadyUntil.set(cacheKey, this.now() + ttlMs);
1075
+ alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: false, cachedForMs: ttlMs });
1076
+ }
1077
+ }
1078
+ /** Run the full 8-step flow and resolve with the resource body. */
1079
+ async pay402(opts) {
1080
+ const { resourceUrl, requirement, signal } = opts;
1081
+ const extra = requirement.extra ?? {};
1082
+ const paymentNeededHeader = String(extra.payment_needed_header ?? "");
1083
+ if (!paymentNeededHeader) {
1084
+ throw new AlipayProtocolError("alipay requirement missing extra.payment_needed_header");
1085
+ }
1086
+ const flow = this.sessionId;
1087
+ const flowStart = this.now();
1088
+ alipayLog.info("flow.start", { flow, resource: resourceUrl });
1089
+ const media = [];
1090
+ const onLine = (line) => {
1091
+ const m = extractMedia(line);
1092
+ if (m) media.push(m);
1093
+ else opts.onLine?.(line);
1094
+ };
1095
+ const intentSummary = opts.intentSummary?.trim() || `\u652F\u4ED8 ${requirement.amount ?? ""} ${requirement.asset ?? "CNY"}`.trim();
1096
+ await timeStep("ensure-cli", flow, () => ensureCli(this.getVersion));
1097
+ const intentTtlMs = resolveIntentTtlMs();
1098
+ const intentKey = `${this.configDir}::${this.framework}`;
1099
+ const useIntentCache = this.intentCacheable && intentTtlMs > 0;
1100
+ const intentUntil = useIntentCache ? intentDoneUntil.get(intentKey) : void 0;
1101
+ if (intentUntil !== void 0 && this.now() < intentUntil) {
1102
+ alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: true, ttlMsLeft: intentUntil - this.now() });
1103
+ } else {
1104
+ await this.runner(
1105
+ [
1106
+ "payment-intent",
1107
+ "--session-id",
1108
+ this.sessionId,
1109
+ "--intent-summary",
1110
+ intentSummary,
1111
+ "--framework",
1112
+ this.framework
1113
+ ],
1114
+ { onLine, signal, step: "payment-intent", flow }
1115
+ );
1116
+ if (useIntentCache) {
1117
+ intentDoneUntil.set(intentKey, this.now() + intentTtlMs);
1118
+ alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: false, cachedForMs: intentTtlMs });
1119
+ }
1120
+ }
1121
+ await this.checkWallet(signal, flow);
1122
+ const reqId = String(extra.out_trade_no ?? randomUUID());
1123
+ const dir = join2(this.configDir, "alipay");
1124
+ await mkdir(dir, { recursive: true });
1125
+ const challengeFile = join2(dir, `402_${reqId}.txt`);
1126
+ await writeFile(challengeFile, paymentNeededHeader, "utf-8");
1127
+ const payArgs = [
1128
+ "402-buyer-pay",
1129
+ "-f",
1130
+ challengeFile,
1131
+ "-r",
1132
+ resourceUrl,
1133
+ "-s",
1134
+ this.sessionId,
1135
+ "-i",
1136
+ intentSummary,
1137
+ "-w",
1138
+ this.framework
1139
+ ];
1140
+ if (opts.method) payArgs.push("-m", opts.method);
1141
+ if (opts.data) payArgs.push("-d", opts.data);
1142
+ const payRun = await this.runner(payArgs, { onLine, signal, step: "402-buyer-pay", flow });
1143
+ const tradeNo = parseTradeNo(payRun.lines);
1144
+ if (!tradeNo) {
1145
+ throw new AlipayProtocolError("402-buyer-pay did not return a tradeNo");
1146
+ }
1147
+ assertTradeNo(tradeNo);
1148
+ const { paymentUrl, shortenUrl } = parsePaymentUrl(payRun.lines);
1149
+ if (paymentUrl) {
1150
+ alipayLog.info("flow.pending", { flow, tradeNo, ms: this.now() - flowStart });
1151
+ opts.onPaymentPending?.({ paymentUrl, shortenUrl, tradeNo });
1152
+ }
1153
+ const pendingAt = this.now();
1154
+ const windowMs = (requirement.maxTimeoutSeconds ?? 30 * 60) * 1e3;
1155
+ const deadline = this.now() + (opts.timeoutMs ?? windowMs);
1156
+ const poll = await pollUntil(tradeNo, resourceUrl, {
1157
+ // No onLine: the status-poll output embeds the resource and must not reach
1158
+ // the log stream — the body is surfaced via the return value below (§9.3).
1159
+ deadline,
1160
+ signal,
1161
+ runner: this.runner,
1162
+ now: this.now,
1163
+ // Re-fetch the resource the same way it was paid (POST + body), else the
1164
+ // status poll defaults to GET and 404s on a POST-only `/execute`.
1165
+ method: opts.method,
1166
+ data: opts.data
1167
+ });
1168
+ alipayLog.info("flow.settled", { flow, tradeNo, ms: this.now() - pendingAt });
1169
+ const body = extractBody(poll.lines);
1170
+ void this.runner(["402-buyer-fulfillment-ack", "-t", tradeNo], {
1171
+ onLine,
1172
+ signal,
1173
+ step: "402-buyer-fulfillment-ack",
1174
+ flow
1175
+ }).catch(() => void 0);
1176
+ return { body, payment: { tradeNo, outTradeNo: reqId }, media };
1177
+ }
1178
+ };
1179
+
1180
+ // src/client/alipay/router.ts
1181
+ var ALIPAY_RAIL = "alipay";
1182
+ function railOf(req) {
1183
+ if (req.scheme === "alipay-aipay" || req.network === ALIPAY_RAIL) return ALIPAY_RAIL;
1184
+ return networkToChainName(req.network) ?? req.network;
1185
+ }
1186
+ function findRail(accepts, rail) {
1187
+ const requirement = accepts.find((r) => railOf(r) === rail);
1188
+ return requirement ? { rail, requirement } : null;
1189
+ }
1190
+ function selectRail(input) {
1191
+ const { serverAccepts, explicitRail, preference, availability } = input;
1192
+ if (!serverAccepts || serverAccepts.length === 0) {
1193
+ throw new UnsupportedRailError(explicitRail ?? "unknown", "Server offered no payment options");
1194
+ }
1195
+ if (explicitRail) {
1196
+ const hit = findRail(serverAccepts, explicitRail);
1197
+ if (!hit) {
1198
+ const offered = serverAccepts.map(railOf);
1199
+ throw new UnsupportedRailError(
1200
+ explicitRail,
1201
+ `Server doesn't accept rail '${explicitRail}'. Offered: ${offered.join(", ")}`
1202
+ );
1203
+ }
1204
+ return hit;
1205
+ }
1206
+ if (preference) {
1207
+ for (const pref of preference) {
1208
+ const hit = findRail(serverAccepts, pref);
1209
+ if (hit) return hit;
1210
+ }
1211
+ }
1212
+ if (availability?.evmReady) {
1213
+ const evm = serverAccepts.find((r) => railOf(r) !== ALIPAY_RAIL);
1214
+ if (evm) return { rail: railOf(evm), requirement: evm };
1215
+ }
1216
+ if (availability?.alipayReady) {
1217
+ const hit = findRail(serverAccepts, ALIPAY_RAIL);
1218
+ if (hit) return hit;
1219
+ }
1220
+ return { rail: railOf(serverAccepts[0]), requirement: serverAccepts[0] };
1221
+ }
1222
+
1223
+ // src/client/node/index.ts
326
1224
  var DEFAULT_CONFIG = {
327
1225
  chain: "base",
328
1226
  limits: {
@@ -335,15 +1233,24 @@ var MoltsPayClient = class {
335
1233
  config;
336
1234
  walletData = null;
337
1235
  wallet = null;
1236
+ signer = null;
338
1237
  todaySpending = 0;
339
1238
  lastSpendingReset = 0;
1239
+ railPreference;
1240
+ alipaySessionId;
340
1241
  constructor(options = {}) {
341
- this.configDir = options.configDir || join2(homedir2(), ".moltspay");
1242
+ this.configDir = options.configDir || join3(homedir3(), ".moltspay");
342
1243
  this.config = this.loadConfig();
1244
+ this.railPreference = options.railPreference ?? this.config.railPreference;
1245
+ this.alipaySessionId = options.alipaySessionId;
343
1246
  this.walletData = this.loadWallet();
344
1247
  this.loadSpending();
345
1248
  if (this.walletData) {
346
- this.wallet = new Wallet(this.walletData.privateKey);
1249
+ this.wallet = new Wallet2(this.walletData.privateKey);
1250
+ const configDir = this.configDir;
1251
+ this.signer = new NodeSigner(this.wallet, {
1252
+ getSolanaKeypair: () => loadSolanaWallet(configDir)
1253
+ });
347
1254
  }
348
1255
  }
349
1256
  /**
@@ -413,6 +1320,9 @@ var MoltsPayClient = class {
413
1320
  * @param options - Payment options (token selection)
414
1321
  */
415
1322
  async pay(serverUrl, service, params, options = {}) {
1323
+ if (options.rail === ALIPAY_RAIL) {
1324
+ return this.payViaAlipay(serverUrl, service, params, options);
1325
+ }
416
1326
  if (!this.wallet || !this.walletData) {
417
1327
  throw new Error("Client not initialized. Run: npx moltspay init");
418
1328
  }
@@ -471,20 +1381,6 @@ var MoltsPayClient = class {
471
1381
  } catch {
472
1382
  throw new Error("Invalid x-payment-required header");
473
1383
  }
474
- const networkToChainName = (network2) => {
475
- if (network2 === "solana:mainnet") return "solana";
476
- if (network2 === "solana:devnet") return "solana_devnet";
477
- const match = network2.match(/^eip155:(\d+)$/);
478
- if (!match) return null;
479
- const chainId = parseInt(match[1]);
480
- if (chainId === 8453) return "base";
481
- if (chainId === 137) return "polygon";
482
- if (chainId === 84532) return "base_sepolia";
483
- if (chainId === 42431) return "tempo_moderato";
484
- if (chainId === 56) return "bnb";
485
- if (chainId === 97) return "bnb_testnet";
486
- return null;
487
- };
488
1384
  const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
489
1385
  const userSpecifiedChain = options.chain;
490
1386
  let selectedChain;
@@ -618,6 +1514,76 @@ Please specify: --chain <chain_name>`
618
1514
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
619
1515
  return result.result || result;
620
1516
  }
1517
+ /**
1518
+ * Pay for a service over the Alipay fiat rail (2.0.0).
1519
+ *
1520
+ * Unlike the crypto path this needs no EVM wallet — it shells out to
1521
+ * alipay-bot via {@link AlipayClient}. Flow: hit the resource with no
1522
+ * payment to get the 402 challenge, confirm the server actually offers the
1523
+ * alipay rail (selectRail), then run the 8-step state machine and return the
1524
+ * resource body.
1525
+ */
1526
+ async payViaAlipay(serverUrl, service, params, options) {
1527
+ const flow = this.alipaySessionId;
1528
+ let executeUrl = `${serverUrl}/execute`;
1529
+ try {
1530
+ const services = await timeStep(
1531
+ "discover-services",
1532
+ flow,
1533
+ () => this.getServices(serverUrl)
1534
+ );
1535
+ const svc = services.services?.find((s) => s.id === service);
1536
+ if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
1537
+ } catch {
1538
+ }
1539
+ const requestBody = options.rawData ? { service, ...params } : { service, params };
1540
+ const bodyJson = JSON.stringify(requestBody);
1541
+ const res = await timeStep(
1542
+ "challenge-402",
1543
+ flow,
1544
+ () => fetch(executeUrl, {
1545
+ method: "POST",
1546
+ headers: { "Content-Type": "application/json" },
1547
+ body: bodyJson
1548
+ })
1549
+ );
1550
+ if (res.status !== 402) {
1551
+ const data = await res.json().catch(() => ({}));
1552
+ if (res.ok && data.result) return data.result;
1553
+ throw new Error(data.error || `Expected 402, got ${res.status}`);
1554
+ }
1555
+ const header = res.headers.get(PAYMENT_REQUIRED_HEADER);
1556
+ if (!header) throw new Error("Missing x-payment-required header on 402");
1557
+ const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
1558
+ const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
1559
+ const { requirement } = selectRail({
1560
+ serverAccepts: accepts,
1561
+ explicitRail: ALIPAY_RAIL,
1562
+ preference: this.railPreference,
1563
+ availability: { evmReady: this.isInitialized }
1564
+ });
1565
+ const onLine = options.onLine ?? ((line) => process.stdout.write(line + "\n"));
1566
+ const alipay = new AlipayClient({
1567
+ sessionId: this.alipaySessionId,
1568
+ configDir: this.configDir
1569
+ });
1570
+ const result = await alipay.pay402({
1571
+ resourceUrl: executeUrl,
1572
+ requirement,
1573
+ method: "POST",
1574
+ data: bodyJson,
1575
+ onLine,
1576
+ onPaymentPending: options.onPaymentPending,
1577
+ timeoutMs: options.timeoutMs,
1578
+ signal: options.signal
1579
+ });
1580
+ try {
1581
+ const json = JSON.parse(result.body);
1582
+ return json.result ?? json;
1583
+ } catch {
1584
+ return { body: result.body, payment: result.payment, media: result.media };
1585
+ }
1586
+ }
621
1587
  /**
622
1588
  * Handle MPP (Machine Payments Protocol) payment flow
623
1589
  * Called when pay() detects WWW-Authenticate header in 402 response
@@ -713,14 +1679,14 @@ Please specify: --chain <chain_name>`
713
1679
  async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
714
1680
  const { to, amount, token, chainName, chain, spender } = paymentDetails;
715
1681
  const tokenConfig = chain.tokens[token];
716
- const provider = new ethers.JsonRpcProvider(chain.rpc);
1682
+ const provider = new ethers2.JsonRpcProvider(chain.rpc);
717
1683
  const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
718
1684
  const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
719
1685
  if (allowance < amountWeiCheck) {
720
1686
  const nativeBalance = await provider.getBalance(this.wallet.address);
721
- const minGasBalance = ethers.parseEther("0.0005");
1687
+ const minGasBalance = ethers2.parseEther("0.0005");
722
1688
  if (nativeBalance < minGasBalance) {
723
- const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
1689
+ const nativeBNB = parseFloat(ethers2.formatEther(nativeBalance)).toFixed(4);
724
1690
  const isTestnet = chainName === "bnb_testnet";
725
1691
  if (isTestnet) {
726
1692
  throw new Error(
@@ -754,35 +1720,21 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
754
1720
  );
755
1721
  }
756
1722
  const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
757
- const intent = {
1723
+ const intentNonce = Date.now();
1724
+ const intentDeadline = Date.now() + 36e5;
1725
+ const envelope = buildBnbIntentTypedData({
758
1726
  from: this.wallet.address,
759
1727
  to,
760
1728
  amount: amountWei,
761
- token: tokenConfig.address,
1729
+ tokenAddress: tokenConfig.address,
762
1730
  service,
763
- nonce: Date.now(),
764
- // Use timestamp as nonce for simplicity
765
- deadline: Date.now() + 36e5
766
- // 1 hour
767
- };
768
- const domain = {
769
- name: "MoltsPay",
770
- version: "1",
1731
+ nonce: intentNonce,
1732
+ deadline: intentDeadline,
771
1733
  chainId: chain.chainId
772
- };
773
- const types = {
774
- PaymentIntent: [
775
- { name: "from", type: "address" },
776
- { name: "to", type: "address" },
777
- { name: "amount", type: "uint256" },
778
- { name: "token", type: "address" },
779
- { name: "service", type: "string" },
780
- { name: "nonce", type: "uint256" },
781
- { name: "deadline", type: "uint256" }
782
- ]
783
- };
1734
+ });
784
1735
  console.log(`[MoltsPay] Signing BNB payment intent...`);
785
- const signature = await this.wallet.signTypedData(domain, types, intent);
1736
+ const signature = await this.signer.signTypedData(envelope);
1737
+ const intent = envelope.message;
786
1738
  const network = `eip155:${chain.chainId}`;
787
1739
  const payload = {
788
1740
  x402Version: 2,
@@ -856,12 +1808,11 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
856
1808
  feePayerPubkey
857
1809
  // Optional fee payer for gasless mode
858
1810
  );
859
- if (feePayerPubkey) {
860
- transaction.partialSign(solanaWallet);
861
- } else {
862
- transaction.sign(solanaWallet);
863
- }
864
- const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
1811
+ const unsignedBase64 = transaction.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
1812
+ const signedTx = await this.signer.signSolanaTransaction({
1813
+ transactionBase64: unsignedBase64,
1814
+ partialSign: !!feePayerPubkey
1815
+ });
865
1816
  console.log(`[MoltsPay] Transaction signed, sending to server...`);
866
1817
  const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
867
1818
  const payload = {
@@ -908,7 +1859,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
908
1859
  * Check ERC20 allowance for a spender
909
1860
  */
910
1861
  async checkAllowance(tokenAddress, spender, provider) {
911
- const contract = new ethers.Contract(
1862
+ const contract = new ethers2.Contract(
912
1863
  tokenAddress,
913
1864
  ["function allowance(address owner, address spender) view returns (uint256)"],
914
1865
  provider
@@ -919,41 +1870,29 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
919
1870
  * Sign EIP-3009 transferWithAuthorization (GASLESS)
920
1871
  * This only signs - no on-chain transaction, no gas needed.
921
1872
  * Supports both USDC and USDT.
1873
+ *
1874
+ * Delegates typed-data construction to `core/eip3009.ts` and the signature
1875
+ * itself to `this.signer`. That way Web Client (Phase 4) can reuse the same
1876
+ * flow with an EIP-1193 signer without duplicating typed-data layout.
922
1877
  */
923
1878
  async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
924
- const validAfter = 0;
925
- const validBefore = Math.floor(Date.now() / 1e3) + 3600;
926
- const nonce = ethers.hexlify(ethers.randomBytes(32));
927
1879
  const tokenConfig = chain.tokens[token];
928
1880
  const value = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
929
- const authorization = {
1881
+ const nonce = ethers2.hexlify(ethers2.randomBytes(32));
1882
+ const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
1883
+ const tokenVersion = domainOverride?.version || "2";
1884
+ const envelope = buildEIP3009TypedData({
930
1885
  from: this.wallet.address,
931
1886
  to,
932
1887
  value,
933
- validAfter: validAfter.toString(),
934
- validBefore: validBefore.toString(),
935
- nonce
936
- };
937
- const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
938
- const tokenVersion = domainOverride?.version || "2";
939
- const domain = {
940
- name: tokenName,
941
- version: tokenVersion,
1888
+ nonce,
942
1889
  chainId: chain.chainId,
943
- verifyingContract: tokenConfig.address
944
- };
945
- const types = {
946
- TransferWithAuthorization: [
947
- { name: "from", type: "address" },
948
- { name: "to", type: "address" },
949
- { name: "value", type: "uint256" },
950
- { name: "validAfter", type: "uint256" },
951
- { name: "validBefore", type: "uint256" },
952
- { name: "nonce", type: "bytes32" }
953
- ]
954
- };
955
- const signature = await this.wallet.signTypedData(domain, types, authorization);
956
- return { authorization, signature };
1890
+ tokenAddress: tokenConfig.address,
1891
+ tokenName,
1892
+ tokenVersion
1893
+ });
1894
+ const signature = await this.signer.signTypedData(envelope);
1895
+ return { authorization: envelope.message, signature };
957
1896
  }
958
1897
  /**
959
1898
  * Check spending limits
@@ -985,7 +1924,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
985
1924
  }
986
1925
  // --- Config & Wallet Management ---
987
1926
  loadConfig() {
988
- const configPath = join2(this.configDir, "config.json");
1927
+ const configPath = join3(this.configDir, "config.json");
989
1928
  if (existsSync2(configPath)) {
990
1929
  const content = readFileSync2(configPath, "utf-8");
991
1930
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
@@ -994,14 +1933,14 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
994
1933
  }
995
1934
  saveConfig() {
996
1935
  mkdirSync2(this.configDir, { recursive: true });
997
- const configPath = join2(this.configDir, "config.json");
1936
+ const configPath = join3(this.configDir, "config.json");
998
1937
  writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
999
1938
  }
1000
1939
  /**
1001
1940
  * Load spending data from disk
1002
1941
  */
1003
1942
  loadSpending() {
1004
- const spendingPath = join2(this.configDir, "spending.json");
1943
+ const spendingPath = join3(this.configDir, "spending.json");
1005
1944
  if (existsSync2(spendingPath)) {
1006
1945
  try {
1007
1946
  const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
@@ -1024,7 +1963,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1024
1963
  */
1025
1964
  saveSpending() {
1026
1965
  mkdirSync2(this.configDir, { recursive: true });
1027
- const spendingPath = join2(this.configDir, "spending.json");
1966
+ const spendingPath = join3(this.configDir, "spending.json");
1028
1967
  const data = {
1029
1968
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
1030
1969
  amount: this.todaySpending,
@@ -1033,7 +1972,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1033
1972
  writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
1034
1973
  }
1035
1974
  loadWallet() {
1036
- const walletPath = join2(this.configDir, "wallet.json");
1975
+ const walletPath = join3(this.configDir, "wallet.json");
1037
1976
  if (existsSync2(walletPath)) {
1038
1977
  if (process.platform !== "win32") {
1039
1978
  try {
@@ -1057,13 +1996,13 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1057
1996
  */
1058
1997
  static init(configDir, options) {
1059
1998
  mkdirSync2(configDir, { recursive: true });
1060
- const wallet = Wallet.createRandom();
1999
+ const wallet = Wallet2.createRandom();
1061
2000
  const walletData = {
1062
2001
  address: wallet.address,
1063
2002
  privateKey: wallet.privateKey,
1064
2003
  createdAt: Date.now()
1065
2004
  };
1066
- const walletPath = join2(configDir, "wallet.json");
2005
+ const walletPath = join3(configDir, "wallet.json");
1067
2006
  writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1068
2007
  const config = {
1069
2008
  chain: options.chain,
@@ -1072,7 +2011,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1072
2011
  maxPerDay: options.maxPerDay
1073
2012
  }
1074
2013
  };
1075
- const configPath = join2(configDir, "config.json");
2014
+ const configPath = join3(configDir, "config.json");
1076
2015
  writeFileSync2(configPath, JSON.stringify(config, null, 2));
1077
2016
  return { address: wallet.address, configDir };
1078
2017
  }
@@ -1089,17 +2028,17 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1089
2028
  } catch {
1090
2029
  throw new Error(`Unknown chain: ${this.config.chain}`);
1091
2030
  }
1092
- const provider = new ethers.JsonRpcProvider(chain.rpc);
2031
+ const provider = new ethers2.JsonRpcProvider(chain.rpc);
1093
2032
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
1094
2033
  const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
1095
2034
  provider.getBalance(this.wallet.address),
1096
- new ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
1097
- new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
2035
+ new ethers2.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
2036
+ new ethers2.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
1098
2037
  ]);
1099
2038
  return {
1100
- usdc: parseFloat(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
1101
- usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
1102
- native: parseFloat(ethers.formatEther(nativeBalance))
2039
+ usdc: parseFloat(ethers2.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
2040
+ usdt: parseFloat(ethers2.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
2041
+ native: parseFloat(ethers2.formatEther(nativeBalance))
1103
2042
  };
1104
2043
  }
1105
2044
  /**
@@ -1122,38 +2061,38 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1122
2061
  supportedChains.map(async (chainName) => {
1123
2062
  try {
1124
2063
  const chain = getChain(chainName);
1125
- const provider = new ethers.JsonRpcProvider(chain.rpc);
2064
+ const provider = new ethers2.JsonRpcProvider(chain.rpc);
1126
2065
  if (chainName === "tempo_moderato") {
1127
2066
  const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
1128
2067
  provider.getBalance(this.wallet.address),
1129
- new ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1130
- new ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1131
- new ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1132
- new ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
2068
+ new ethers2.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
2069
+ new ethers2.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
2070
+ new ethers2.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
2071
+ new ethers2.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
1133
2072
  ]);
1134
2073
  results[chainName] = {
1135
- usdc: parseFloat(ethers.formatUnits(pathUSD, 6)),
2074
+ usdc: parseFloat(ethers2.formatUnits(pathUSD, 6)),
1136
2075
  // pathUSD as default USDC
1137
- usdt: parseFloat(ethers.formatUnits(alphaUSD, 6)),
2076
+ usdt: parseFloat(ethers2.formatUnits(alphaUSD, 6)),
1138
2077
  // alphaUSD as default USDT
1139
- native: parseFloat(ethers.formatEther(nativeBalance)),
2078
+ native: parseFloat(ethers2.formatEther(nativeBalance)),
1140
2079
  tempo: {
1141
- pathUSD: parseFloat(ethers.formatUnits(pathUSD, 6)),
1142
- alphaUSD: parseFloat(ethers.formatUnits(alphaUSD, 6)),
1143
- betaUSD: parseFloat(ethers.formatUnits(betaUSD, 6)),
1144
- thetaUSD: parseFloat(ethers.formatUnits(thetaUSD, 6))
2080
+ pathUSD: parseFloat(ethers2.formatUnits(pathUSD, 6)),
2081
+ alphaUSD: parseFloat(ethers2.formatUnits(alphaUSD, 6)),
2082
+ betaUSD: parseFloat(ethers2.formatUnits(betaUSD, 6)),
2083
+ thetaUSD: parseFloat(ethers2.formatUnits(thetaUSD, 6))
1145
2084
  }
1146
2085
  };
1147
2086
  } else {
1148
2087
  const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
1149
2088
  provider.getBalance(this.wallet.address),
1150
- new ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
1151
- new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
2089
+ new ethers2.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
2090
+ new ethers2.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
1152
2091
  ]);
1153
2092
  results[chainName] = {
1154
- usdc: parseFloat(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
1155
- usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
1156
- native: parseFloat(ethers.formatEther(nativeBalance))
2093
+ usdc: parseFloat(ethers2.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
2094
+ usdt: parseFloat(ethers2.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
2095
+ native: parseFloat(ethers2.formatEther(nativeBalance))
1157
2096
  };
1158
2097
  }
1159
2098
  } catch (err2) {
@@ -1311,9 +2250,9 @@ function wrap(fn) {
1311
2250
  }
1312
2251
  function getPackageVersion() {
1313
2252
  const candidates = [
1314
- join3(__dirname, "../../package.json"),
1315
- join3(__dirname, "../package.json"),
1316
- join3(process.cwd(), "node_modules/moltspay/package.json")
2253
+ join4(__dirname, "../../package.json"),
2254
+ join4(__dirname, "../package.json"),
2255
+ join4(process.cwd(), "node_modules/moltspay/package.json")
1317
2256
  ];
1318
2257
  for (const loc of candidates) {
1319
2258
  try {