moltspay 1.6.0 → 2.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 (57) hide show
  1. package/.env.example +23 -0
  2. package/CHANGELOG.md +551 -0
  3. package/README.md +466 -7
  4. package/dist/cdp/index.js.map +1 -1
  5. package/dist/cdp/index.mjs.map +1 -1
  6. package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
  7. package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
  8. package/dist/chains/index.d.mts +102 -1
  9. package/dist/chains/index.d.ts +102 -1
  10. package/dist/chains/index.js +66 -0
  11. package/dist/chains/index.js.map +1 -1
  12. package/dist/chains/index.mjs +57 -0
  13. package/dist/chains/index.mjs.map +1 -1
  14. package/dist/cli/index.js +6850 -2127
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli/index.mjs +6857 -2128
  17. package/dist/cli/index.mjs.map +1 -1
  18. package/dist/client/index.d.mts +348 -1
  19. package/dist/client/index.d.ts +348 -1
  20. package/dist/client/index.js +1462 -43
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/index.mjs +1469 -51
  23. package/dist/client/index.mjs.map +1 -1
  24. package/dist/client/web/index.d.mts +17 -1
  25. package/dist/client/web/index.mjs +11 -0
  26. package/dist/client/web/index.mjs.map +1 -1
  27. package/dist/facilitators/index.d.mts +663 -4
  28. package/dist/facilitators/index.d.ts +663 -4
  29. package/dist/facilitators/index.js +1292 -10
  30. package/dist/facilitators/index.js.map +1 -1
  31. package/dist/facilitators/index.mjs +1268 -9
  32. package/dist/facilitators/index.mjs.map +1 -1
  33. package/dist/index.d.mts +78 -3
  34. package/dist/index.d.ts +78 -3
  35. package/dist/index.js +4229 -556
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +4214 -548
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/mcp/index.js +1462 -45
  40. package/dist/mcp/index.js.map +1 -1
  41. package/dist/mcp/index.mjs +1473 -56
  42. package/dist/mcp/index.mjs.map +1 -1
  43. package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
  44. package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
  45. package/dist/server/index.d.mts +336 -2
  46. package/dist/server/index.d.ts +336 -2
  47. package/dist/server/index.js +2516 -188
  48. package/dist/server/index.js.map +1 -1
  49. package/dist/server/index.mjs +2516 -188
  50. package/dist/server/index.mjs.map +1 -1
  51. package/dist/verify/index.js.map +1 -1
  52. package/dist/verify/index.mjs.map +1 -1
  53. package/dist/wallet/index.js.map +1 -1
  54. package/dist/wallet/index.mjs.map +1 -1
  55. package/package.json +20 -2
  56. package/schemas/moltspay.services.schema.json +211 -6
  57. package/scripts/postinstall.js +91 -0
@@ -1,8 +1,9 @@
1
1
  // src/client/node/index.ts
2
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
3
- import { homedir as homedir2 } from "os";
4
- import { join as join2 } from "path";
5
- import { Wallet as Wallet2, ethers as ethers2 } from "ethers";
2
+ import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, statSync, chmodSync, readdirSync as readdirSync2 } from "fs";
3
+ import { homedir as homedir4 } from "os";
4
+ import { join as join4 } from "path";
5
+ import { randomUUID as randomUUID3 } from "crypto";
6
+ import { Wallet as Wallet2, ethers as ethers3 } from "ethers";
6
7
 
7
8
  // src/chains/index.ts
8
9
  var CHAINS = {
@@ -328,6 +329,16 @@ function networkToChainName(network) {
328
329
  // src/client/core/base64.ts
329
330
  var BufferCtor = globalThis.Buffer;
330
331
 
332
+ // src/client/core/errors.ts
333
+ var MoltsPayError = class extends Error {
334
+ code;
335
+ constructor(code, message) {
336
+ super(message);
337
+ this.name = "MoltsPayError";
338
+ this.code = code;
339
+ }
340
+ };
341
+
331
342
  // src/client/core/eip3009.ts
332
343
  var EIP3009_TYPES = {
333
344
  TransferWithAuthorization: [
@@ -464,6 +475,936 @@ function findChainByChainId(chainId) {
464
475
  return void 0;
465
476
  }
466
477
 
478
+ // src/client/alipay/index.ts
479
+ import { randomUUID } from "crypto";
480
+ import { mkdir, writeFile } from "fs/promises";
481
+ import { homedir as homedir2 } from "os";
482
+ import { join as join2 } from "path";
483
+
484
+ // src/client/alipay/cli.ts
485
+ import { spawn } from "child_process";
486
+
487
+ // src/client/alipay/log.ts
488
+ var RANK = { off: 0, info: 1, debug: 2 };
489
+ function normalizeLevel(v) {
490
+ const s = (v ?? "").toLowerCase();
491
+ return s === "info" || s === "debug" ? s : "off";
492
+ }
493
+ var level = normalizeLevel(process.env.MOLTSPAY_ALIPAY_LOG);
494
+ var sink = (line) => {
495
+ process.stderr.write(line + "\n");
496
+ };
497
+ function enabledFor(want) {
498
+ return RANK[level] >= RANK[want];
499
+ }
500
+ function render(event, fields) {
501
+ const parts = ["[moltspay:alipay]", String(fields.ts), event];
502
+ if (fields.flow) parts.push(`flow=${fields.flow}`);
503
+ if (fields.step) parts.push(`step=${fields.step}`);
504
+ if (typeof fields.ms === "number") parts.push(`${fields.ms}ms`);
505
+ for (const [k, v] of Object.entries(fields)) {
506
+ if (["ts", "level", "event", "flow", "step", "ms"].includes(k)) continue;
507
+ parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
508
+ }
509
+ return parts.join(" ");
510
+ }
511
+ function emit(lvl, event, fields) {
512
+ if (!enabledFor(lvl)) return;
513
+ const full = { ts: (/* @__PURE__ */ new Date()).toISOString(), level: lvl, event, ...fields };
514
+ try {
515
+ sink(render(event, full), full);
516
+ } catch {
517
+ }
518
+ }
519
+ var alipayLog = {
520
+ info: (event, fields = {}) => emit("info", event, fields),
521
+ debug: (event, fields = {}) => emit("debug", event, fields),
522
+ /** True if anything at all would be logged (cheap guard for hot paths). */
523
+ enabled: () => level !== "off"
524
+ };
525
+ async function timeStep(step, flow, fn) {
526
+ if (!alipayLog.enabled()) return fn();
527
+ const t0 = Date.now();
528
+ alipayLog.debug("step.start", { flow, step });
529
+ try {
530
+ const out = await fn();
531
+ alipayLog.info("step.end", { flow, step, ms: Date.now() - t0, ok: true });
532
+ return out;
533
+ } catch (err) {
534
+ alipayLog.info("step.end", {
535
+ flow,
536
+ step,
537
+ ms: Date.now() - t0,
538
+ ok: false,
539
+ err: err instanceof Error ? err.message : String(err)
540
+ });
541
+ throw err;
542
+ }
543
+ }
544
+
545
+ // src/client/alipay/cli.ts
546
+ var ALLOWED_ENV = /* @__PURE__ */ new Set([
547
+ "AIPAY_OUTPUT_CHANNEL",
548
+ "AIPAY_SESSION_ID",
549
+ "AIPAY_FRAMEWORK",
550
+ "AIPAY_MODEL",
551
+ "AIPAY_OS",
552
+ // Minimal survival set for spawn to find the binary and a home dir.
553
+ "PATH",
554
+ "HOME"
555
+ ]);
556
+ function filterEnv(env) {
557
+ return Object.fromEntries(
558
+ Object.entries(env).filter(([k]) => ALLOWED_ENV.has(k))
559
+ );
560
+ }
561
+ function makeLineSplitter(onLine) {
562
+ let buf = "";
563
+ return (chunk) => {
564
+ buf += chunk.toString("utf-8");
565
+ let nl;
566
+ while ((nl = buf.indexOf("\n")) !== -1) {
567
+ onLine(buf.slice(0, nl));
568
+ buf = buf.slice(nl + 1);
569
+ }
570
+ };
571
+ }
572
+ function profileEnv(step, flow) {
573
+ const want = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE;
574
+ const hook = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_HOOK;
575
+ if (!want || !hook) return {};
576
+ const steps = want.split(",").map((s) => s.trim());
577
+ if (!steps.includes("all") && !steps.includes(step)) return {};
578
+ const dir = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_DIR || "/tmp";
579
+ const safe = `${flow ?? "noflow"}-${step}-${Date.now()}`.replace(/[^a-zA-Z0-9._-]/g, "_");
580
+ const out = `${dir}/cli-profile-${safe}.json`;
581
+ alipayLog.info("cli.profile", { flow, step, out });
582
+ return {
583
+ NODE_OPTIONS: `--require=${hook}`,
584
+ MOLTSPAY_CLI_PROFILE_OUT: out
585
+ };
586
+ }
587
+ var runCli = (args, opts = {}) => {
588
+ const bin = opts.bin ?? "alipay-bot";
589
+ const step = opts.step ?? args[0] ?? "(no-arg)";
590
+ const startedAt = Date.now();
591
+ const lines = [];
592
+ const collect = (line) => {
593
+ lines.push(line);
594
+ alipayLog.debug("cli.line", {
595
+ flow: opts.flow,
596
+ step,
597
+ ms: Date.now() - startedAt,
598
+ preview: line.slice(0, 120)
599
+ });
600
+ opts.onLine?.(line);
601
+ };
602
+ alipayLog.debug("step.start", { flow: opts.flow, step });
603
+ return new Promise((resolve, reject) => {
604
+ const child = spawn(bin, args, {
605
+ env: { ...filterEnv(process.env), ...profileEnv(step, opts.flow), ...opts.env ?? {} }
606
+ });
607
+ if (opts.signal) {
608
+ if (opts.signal.aborted) child.kill("SIGTERM");
609
+ opts.signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
610
+ }
611
+ let sawByte = false;
612
+ const onChunk = (stream) => (chunk) => {
613
+ const ms = Date.now() - startedAt;
614
+ if (!sawByte) {
615
+ sawByte = true;
616
+ alipayLog.debug("cli.firstbyte", { flow: opts.flow, step, ms });
617
+ }
618
+ alipayLog.debug("cli.chunk", { flow: opts.flow, step, ms, stream, bytes: chunk.length });
619
+ };
620
+ const onStdout = makeLineSplitter(collect);
621
+ const onStderr = makeLineSplitter(collect);
622
+ child.stdout?.on("data", onChunk("out"));
623
+ child.stderr?.on("data", onChunk("err"));
624
+ child.stdout?.on("data", onStdout);
625
+ child.stderr?.on("data", onStderr);
626
+ child.on("error", (err) => {
627
+ alipayLog.info("cli.exit", {
628
+ flow: opts.flow,
629
+ step,
630
+ ms: Date.now() - startedAt,
631
+ ok: false,
632
+ err: err.message
633
+ });
634
+ reject(err);
635
+ });
636
+ child.on("close", (code) => {
637
+ alipayLog.info("cli.exit", {
638
+ flow: opts.flow,
639
+ step,
640
+ ms: Date.now() - startedAt,
641
+ ok: (code ?? 1) === 0,
642
+ code: code ?? 1
643
+ });
644
+ resolve({ exitCode: code ?? 1, lines });
645
+ });
646
+ });
647
+ };
648
+
649
+ // src/client/alipay/install.ts
650
+ import { execFile } from "child_process";
651
+ import { promisify } from "util";
652
+
653
+ // src/client/alipay/errors.ts
654
+ var AlipayCliNotFoundError = class extends MoltsPayError {
655
+ constructor(message) {
656
+ super("ALIPAY_CLI_NOT_FOUND", message);
657
+ this.name = "AlipayCliNotFoundError";
658
+ }
659
+ };
660
+ var AlipayCliVersionError = class extends MoltsPayError {
661
+ constructor(message) {
662
+ super("ALIPAY_CLI_VERSION", message);
663
+ this.name = "AlipayCliVersionError";
664
+ }
665
+ };
666
+ var NeedsWalletSetupError = class extends MoltsPayError {
667
+ constructor(message = "Alipay wallet not set up. Run: moltspay alipay apply") {
668
+ super("ALIPAY_NEEDS_WALLET_SETUP", message);
669
+ this.name = "NeedsWalletSetupError";
670
+ }
671
+ };
672
+ var AlipayPaymentRejectedError = class extends MoltsPayError {
673
+ constructor(message) {
674
+ super("ALIPAY_PAYMENT_REJECTED", message);
675
+ this.name = "AlipayPaymentRejectedError";
676
+ }
677
+ };
678
+ var AlipayPaymentTimeoutError = class extends MoltsPayError {
679
+ constructor(message) {
680
+ super("ALIPAY_PAYMENT_TIMEOUT", message);
681
+ this.name = "AlipayPaymentTimeoutError";
682
+ }
683
+ };
684
+ var AlipayProtocolError = class extends MoltsPayError {
685
+ constructor(message) {
686
+ super("ALIPAY_PROTOCOL", message);
687
+ this.name = "AlipayProtocolError";
688
+ }
689
+ };
690
+ var UnsupportedRailError = class extends MoltsPayError {
691
+ constructor(rail, message) {
692
+ super("UNSUPPORTED_RAIL", message ?? `Rail not supported by server: ${rail}`);
693
+ this.rail = rail;
694
+ this.name = "UnsupportedRailError";
695
+ }
696
+ };
697
+
698
+ // src/client/alipay/install.ts
699
+ var execFileAsync = promisify(execFile);
700
+ var MIN_CLI_VERSION = "0.3.15";
701
+ function semverLt(a, b) {
702
+ const pa = a.split(".").map((n) => parseInt(n, 10));
703
+ const pb = b.split(".").map((n) => parseInt(n, 10));
704
+ for (let i = 0; i < 3; i++) {
705
+ const x = pa[i] ?? 0;
706
+ const y = pb[i] ?? 0;
707
+ if (x !== y) return x < y;
708
+ }
709
+ return false;
710
+ }
711
+ function parseVersion(stdout) {
712
+ return stdout.match(/v?(\d+\.\d+\.\d+)/)?.[1] ?? null;
713
+ }
714
+ var defaultGetVersion = async () => {
715
+ const { stdout } = await execFileAsync("alipay-bot", ["--version"]);
716
+ return stdout;
717
+ };
718
+ async function ensureCliUncached(getVersion) {
719
+ let stdout;
720
+ try {
721
+ stdout = await getVersion();
722
+ } catch (e) {
723
+ if (e?.code === "ENOENT") {
724
+ throw new AlipayCliNotFoundError(
725
+ "alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
726
+ );
727
+ }
728
+ throw e;
729
+ }
730
+ const version = parseVersion(stdout);
731
+ if (!version || semverLt(version, MIN_CLI_VERSION)) {
732
+ throw new AlipayCliVersionError(
733
+ `alipay-bot ${version ?? "?"} found, need \u2265 ${MIN_CLI_VERSION}. Run: npx -y @alipay/agent-payment@latest update`
734
+ );
735
+ }
736
+ return version;
737
+ }
738
+ var cachedVersion = null;
739
+ var inflight = null;
740
+ async function ensureCli(getVersion = defaultGetVersion) {
741
+ if (getVersion !== defaultGetVersion) {
742
+ return ensureCliUncached(getVersion);
743
+ }
744
+ if (cachedVersion) return cachedVersion;
745
+ if (!inflight) {
746
+ inflight = ensureCliUncached(getVersion).then((v) => {
747
+ cachedVersion = v;
748
+ return v;
749
+ }).finally(() => {
750
+ inflight = null;
751
+ });
752
+ }
753
+ return inflight;
754
+ }
755
+
756
+ // src/client/alipay/poll.ts
757
+ var POLL_INTERVAL_MS = 3e3;
758
+ var POLL_MAX_INFLIGHT = 2;
759
+ function resolveMaxInflight(override) {
760
+ if (override !== void 0) return Math.max(1, Math.floor(override));
761
+ const raw = process.env.MOLTSPAY_ALIPAY_POLL_MAX_INFLIGHT;
762
+ if (raw !== void 0 && raw.trim() !== "") {
763
+ const n = Number(raw);
764
+ if (Number.isFinite(n) && n >= 1) return Math.floor(n);
765
+ }
766
+ return POLL_MAX_INFLIGHT;
767
+ }
768
+ function resolveLaunchGap(override) {
769
+ if (override !== void 0) return Math.max(0, override);
770
+ const raw = process.env.MOLTSPAY_ALIPAY_POLL_LAUNCH_MS;
771
+ if (raw !== void 0 && raw.trim() !== "") {
772
+ const n = Number(raw);
773
+ if (Number.isFinite(n) && n >= 0) return n;
774
+ }
775
+ return POLL_INTERVAL_MS;
776
+ }
777
+ function parseStatus(lines) {
778
+ const raw = lines.join("\n").trim();
779
+ try {
780
+ const json = JSON.parse(raw);
781
+ if (json && typeof json === "object") {
782
+ if (typeof json.success === "boolean") {
783
+ if (json.success) return "paid";
784
+ const err = String(json.errorCode ?? "").toUpperCase();
785
+ if (/UNPAID|WAIT|PENDING|PROCESS|NOTPAY/.test(err)) return "pending";
786
+ if (/CLOSED|CANCEL|FAIL|REJECT|REFUSE|TIMEOUT|EXPIRE/.test(err)) return "rejected";
787
+ return "pending";
788
+ }
789
+ if (typeof json.code !== "undefined") {
790
+ if (Number(json.code) === 200) return "paid";
791
+ const msg = `${json.message ?? ""}${json.reason ?? ""}`;
792
+ if (/关闭|失败|拒绝|取消|超时|已撤销/.test(msg)) return "rejected";
793
+ return "pending";
794
+ }
795
+ if (typeof json.body === "string") {
796
+ return classifyReportText(json.body);
797
+ }
798
+ }
799
+ } catch {
800
+ }
801
+ return classifyReportText(raw);
802
+ }
803
+ function classifyReportText(s) {
804
+ const text = s.toUpperCase();
805
+ if (/查询支付状态成功并获取资源/.test(s) || /资源响应状态[^0-9\n]{0,8}200/.test(s) || /TRADE_SUCCESS|TRADE_FINISHED|"STATUS":\s*"FULFILLED"/.test(text)) return "paid";
806
+ if (/TRADE_CLOSED|REJECTED|REFUSE|CANCEL/.test(text) || /交易关闭|支付失败|已拒绝|已取消|已撤销|交易超时/.test(s)) return "rejected";
807
+ if (/WAIT_BUYER_PAY|UNPAID|PENDING|WAITING/.test(text) || /交易未支付|等待付款|待支付|未支付/.test(s)) return "pending";
808
+ return "unknown";
809
+ }
810
+ function defaultSleep(ms, signal) {
811
+ return new Promise((resolve, reject) => {
812
+ if (signal?.aborted) return reject(new Error("aborted"));
813
+ const t = setTimeout(resolve, ms);
814
+ signal?.addEventListener(
815
+ "abort",
816
+ () => {
817
+ clearTimeout(t);
818
+ reject(new Error("aborted"));
819
+ },
820
+ { once: true }
821
+ );
822
+ });
823
+ }
824
+ var LAUNCH = /* @__PURE__ */ Symbol("launch");
825
+ async function pollUntil(tradeNo, resourceUrl, opts) {
826
+ const runner = opts.runner ?? runCli;
827
+ const sleep = opts.sleep ?? defaultSleep;
828
+ const now = opts.now ?? Date.now;
829
+ const maxInflight = resolveMaxInflight(opts.maxInflight);
830
+ const launchGap = resolveLaunchGap(opts.launchIntervalMs);
831
+ const args = [
832
+ "402-query-payment-status",
833
+ "-t",
834
+ tradeNo,
835
+ "-r",
836
+ resourceUrl,
837
+ ...opts.method ? ["-m", opts.method] : [],
838
+ ...opts.data ? ["-d", opts.data] : []
839
+ ];
840
+ const internal = new AbortController();
841
+ const signal = opts.signal ? AbortSignal.any([opts.signal, internal.signal]) : internal.signal;
842
+ let tick = 0;
843
+ let lastLaunch = -Infinity;
844
+ const inflight2 = /* @__PURE__ */ new Set();
845
+ const launch = () => {
846
+ tick += 1;
847
+ const myTick = tick;
848
+ lastLaunch = now();
849
+ const run = (async () => {
850
+ const { lines } = await runner(args, {
851
+ signal,
852
+ step: "query-payment-status",
853
+ flow: tradeNo
854
+ });
855
+ const status = parseStatus(lines);
856
+ alipayLog.debug("poll.tick", { flow: tradeNo, tick: myTick, status });
857
+ return { status, lines };
858
+ })();
859
+ const tracked = run.finally(() => {
860
+ inflight2.delete(tracked);
861
+ });
862
+ inflight2.add(tracked);
863
+ };
864
+ try {
865
+ for (; ; ) {
866
+ if (opts.signal?.aborted) throw new Error("aborted");
867
+ const expired = now() >= opts.deadline;
868
+ if (expired && inflight2.size === 0) {
869
+ throw new AlipayPaymentTimeoutError(
870
+ `Payment ${tradeNo} not completed before pay_before deadline`
871
+ );
872
+ }
873
+ while (!expired && inflight2.size < maxInflight && now() - lastLaunch >= launchGap) {
874
+ launch();
875
+ }
876
+ const waiters = [...inflight2];
877
+ if (!expired && inflight2.size < maxInflight) {
878
+ const untilNext = Math.max(0, launchGap - (now() - lastLaunch));
879
+ waiters.push(sleep(untilNext, signal).then(() => LAUNCH, () => LAUNCH));
880
+ }
881
+ const winner = await Promise.race(waiters);
882
+ if (winner === LAUNCH) continue;
883
+ if (winner.status === "paid") return { status: "paid", lines: winner.lines };
884
+ if (winner.status === "rejected") {
885
+ throw new AlipayPaymentRejectedError(`Payment ${tradeNo} was rejected`);
886
+ }
887
+ }
888
+ } finally {
889
+ internal.abort();
890
+ for (const p of inflight2) p.catch(() => void 0);
891
+ }
892
+ }
893
+
894
+ // src/client/alipay/index.ts
895
+ var TRADE_NO_RE = /^\d{32}$/;
896
+ function resolveSessionId(explicit, envSession) {
897
+ return explicit ?? envSession ?? randomUUID();
898
+ }
899
+ function assertTradeNo(t) {
900
+ if (!TRADE_NO_RE.test(t)) {
901
+ throw new AlipayProtocolError(`invalid tradeNo (expect 32 digits): ${t}`);
902
+ }
903
+ }
904
+ function parseTradeNo(lines) {
905
+ for (const line of lines) {
906
+ const labeled = line.match(/trade[_-]?no["'\s:=]+(\d{32})/i);
907
+ if (labeled) return labeled[1];
908
+ const bare = line.match(/\b(\d{32})\b/);
909
+ if (bare) return bare[1];
910
+ }
911
+ return null;
912
+ }
913
+ function parsePaymentUrl(lines) {
914
+ let paymentUrl;
915
+ let shortenUrl;
916
+ for (const line of lines) {
917
+ const m = line.match(/(alipays?:\/\/\S+|https?:\/\/\S+)/i);
918
+ if (!m) continue;
919
+ const url = m[1].replace(/[)\]`>]+$/, "");
920
+ if (/short|qr\.alipay|surl|\/s\//i.test(line) && !shortenUrl) shortenUrl = url;
921
+ else if (!paymentUrl) paymentUrl = url;
922
+ }
923
+ if (!paymentUrl && shortenUrl) paymentUrl = shortenUrl;
924
+ return { paymentUrl, shortenUrl };
925
+ }
926
+ function extractMedia(line) {
927
+ const m = line.match(/^\s*MEDIA:\s*(.+?)\s*$/);
928
+ return m ? m[1] : null;
929
+ }
930
+ function isWalletReady(lines) {
931
+ const text = lines.join("\n").trim();
932
+ try {
933
+ const json = JSON.parse(text);
934
+ if (json && typeof json.code !== "undefined") return Number(json.code) === 200;
935
+ } catch {
936
+ }
937
+ return !/未开通|未开启|NOT[_\s-]*(OPEN|BOUND|SET)|NEEDS?[_\s-]*SETUP|NO[_\s-]*WALLET/i.test(text);
938
+ }
939
+ function extractResourceFromReport(report) {
940
+ const label = report.search(/资源响应体/);
941
+ const start = report.indexOf("{", label === -1 ? 0 : label);
942
+ if (start === -1) return void 0;
943
+ let depth = 0, inStr = false, esc = false;
944
+ for (let i = start; i < report.length; i++) {
945
+ const ch = report[i];
946
+ if (inStr) {
947
+ if (esc) esc = false;
948
+ else if (ch === "\\") esc = true;
949
+ else if (ch === '"') inStr = false;
950
+ continue;
951
+ }
952
+ if (ch === '"') inStr = true;
953
+ else if (ch === "{") depth++;
954
+ else if (ch === "}" && --depth === 0) {
955
+ const slice = report.slice(start, i + 1);
956
+ try {
957
+ const obj = JSON.parse(slice);
958
+ const r = obj?.resourceResponse ?? obj?.result ?? obj?.data ?? obj?.body ?? obj;
959
+ return typeof r === "string" ? r : JSON.stringify(r);
960
+ } catch {
961
+ return slice;
962
+ }
963
+ }
964
+ }
965
+ return void 0;
966
+ }
967
+ function extractBody(lines) {
968
+ const text = lines.join("\n").trim();
969
+ try {
970
+ const json = JSON.parse(text);
971
+ if (json && typeof json === "object") {
972
+ if (json.resourceResponse !== void 0 && json.resourceResponse !== null) {
973
+ const rr = json.resourceResponse;
974
+ const r = typeof rr === "object" ? rr.result ?? rr.data ?? rr.body ?? rr : rr;
975
+ return typeof r === "string" ? r : JSON.stringify(r);
976
+ }
977
+ if (typeof json.body === "string") {
978
+ const inner = extractResourceFromReport(json.body);
979
+ return inner ?? json.body;
980
+ }
981
+ const body = json.data ?? json.result ?? json.body ?? json.resource;
982
+ if (body !== void 0) return typeof body === "string" ? body : JSON.stringify(body);
983
+ return text;
984
+ }
985
+ } catch {
986
+ }
987
+ const idx = lines.findIndex((l) => /^\s*BODY:/.test(l));
988
+ if (idx !== -1) {
989
+ const first = lines[idx].replace(/^\s*BODY:\s*/, "");
990
+ return [first, ...lines.slice(idx + 1)].join("\n").trim();
991
+ }
992
+ return lines.filter((l) => !/^\s*(MEDIA|STATUS|INFO):/.test(l)).join("\n").trim();
993
+ }
994
+ var DEFAULT_WALLET_TTL_MS = 10 * 60 * 1e3;
995
+ function resolveWalletTtlMs() {
996
+ const raw = process.env.MOLTSPAY_ALIPAY_WALLET_TTL_MS;
997
+ if (raw === void 0 || raw.trim() === "") return DEFAULT_WALLET_TTL_MS;
998
+ const n = Number(raw);
999
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_WALLET_TTL_MS;
1000
+ }
1001
+ var walletReadyUntil = /* @__PURE__ */ new Map();
1002
+ var DEFAULT_INTENT_TTL_MS = 10 * 60 * 1e3;
1003
+ function resolveIntentTtlMs() {
1004
+ const raw = process.env.MOLTSPAY_ALIPAY_INTENT_TTL_MS;
1005
+ if (raw === void 0 || raw.trim() === "") return DEFAULT_INTENT_TTL_MS;
1006
+ const n = Number(raw);
1007
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTENT_TTL_MS;
1008
+ }
1009
+ var intentDoneUntil = /* @__PURE__ */ new Map();
1010
+ var AlipayClient = class {
1011
+ sessionId;
1012
+ configDir;
1013
+ framework;
1014
+ runner;
1015
+ getVersion;
1016
+ now;
1017
+ /** Only the default runner may use the process-level wallet cache. */
1018
+ walletCacheable;
1019
+ /** Only the default runner may use the process-level payment-intent cache. */
1020
+ intentCacheable;
1021
+ constructor(opts = {}) {
1022
+ this.sessionId = resolveSessionId(opts.sessionId, process.env.AIPAY_SESSION_ID);
1023
+ this.configDir = opts.configDir ?? join2(homedir2(), ".moltspay");
1024
+ this.framework = opts.framework ?? process.env.AIPAY_FRAMEWORK ?? "openclaw";
1025
+ this.runner = opts.runner ?? runCli;
1026
+ this.getVersion = opts.getVersion;
1027
+ this.now = opts.now ?? Date.now;
1028
+ this.walletCacheable = opts.cacheWallet ?? !opts.runner;
1029
+ this.intentCacheable = opts.cacheIntent ?? !opts.runner;
1030
+ }
1031
+ /**
1032
+ * Throws NeedsWalletSetupError unless alipay-bot reports an opened wallet.
1033
+ *
1034
+ * Skips the ~22s `check-wallet` spawn entirely when a prior call cached a
1035
+ * "ready" verdict within the TTL (see {@link DEFAULT_WALLET_TTL_MS}).
1036
+ */
1037
+ async checkWallet(signal, flow) {
1038
+ const ttlMs = resolveWalletTtlMs();
1039
+ const cacheKey = `${this.configDir}::${this.framework}`;
1040
+ const useCache = this.walletCacheable && ttlMs > 0;
1041
+ if (useCache) {
1042
+ const until = walletReadyUntil.get(cacheKey);
1043
+ if (until !== void 0 && this.now() < until) {
1044
+ alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: true, ttlMsLeft: until - this.now() });
1045
+ return;
1046
+ }
1047
+ }
1048
+ const { lines } = await this.runner(["check-wallet"], { signal, step: "check-wallet", flow });
1049
+ if (!isWalletReady(lines)) {
1050
+ if (this.walletCacheable) walletReadyUntil.delete(cacheKey);
1051
+ throw new NeedsWalletSetupError(
1052
+ "Alipay wallet not opened. Run: moltspay alipay apply (then: moltspay alipay bind)"
1053
+ );
1054
+ }
1055
+ if (useCache) {
1056
+ walletReadyUntil.set(cacheKey, this.now() + ttlMs);
1057
+ alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: false, cachedForMs: ttlMs });
1058
+ }
1059
+ }
1060
+ /** Run the full 8-step flow and resolve with the resource body. */
1061
+ async pay402(opts) {
1062
+ const { resourceUrl, requirement, signal } = opts;
1063
+ const extra = requirement.extra ?? {};
1064
+ const paymentNeededHeader = String(extra.payment_needed_header ?? "");
1065
+ if (!paymentNeededHeader) {
1066
+ throw new AlipayProtocolError("alipay requirement missing extra.payment_needed_header");
1067
+ }
1068
+ const flow = this.sessionId;
1069
+ const flowStart = this.now();
1070
+ alipayLog.info("flow.start", { flow, resource: resourceUrl });
1071
+ const media = [];
1072
+ const onLine = (line) => {
1073
+ const m = extractMedia(line);
1074
+ if (m) media.push(m);
1075
+ else opts.onLine?.(line);
1076
+ };
1077
+ const intentSummary = opts.intentSummary?.trim() || `Pay ${requirement.amount ?? ""} ${requirement.asset ?? "CNY"}`.trim();
1078
+ await timeStep("ensure-cli", flow, () => ensureCli(this.getVersion));
1079
+ const intentTtlMs = resolveIntentTtlMs();
1080
+ const intentKey = `${this.configDir}::${this.framework}`;
1081
+ const useIntentCache = this.intentCacheable && intentTtlMs > 0;
1082
+ const intentUntil = useIntentCache ? intentDoneUntil.get(intentKey) : void 0;
1083
+ if (intentUntil !== void 0 && this.now() < intentUntil) {
1084
+ alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: true, ttlMsLeft: intentUntil - this.now() });
1085
+ } else {
1086
+ await this.runner(
1087
+ [
1088
+ "payment-intent",
1089
+ "--session-id",
1090
+ this.sessionId,
1091
+ "--intent-summary",
1092
+ intentSummary,
1093
+ "--framework",
1094
+ this.framework
1095
+ ],
1096
+ { onLine, signal, step: "payment-intent", flow }
1097
+ );
1098
+ if (useIntentCache) {
1099
+ intentDoneUntil.set(intentKey, this.now() + intentTtlMs);
1100
+ alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: false, cachedForMs: intentTtlMs });
1101
+ }
1102
+ }
1103
+ await this.checkWallet(signal, flow);
1104
+ const reqId = String(extra.out_trade_no ?? randomUUID());
1105
+ const dir = join2(this.configDir, "alipay");
1106
+ await mkdir(dir, { recursive: true });
1107
+ const challengeFile = join2(dir, `402_${reqId}.txt`);
1108
+ await writeFile(challengeFile, paymentNeededHeader, "utf-8");
1109
+ const payArgs = [
1110
+ "402-buyer-pay",
1111
+ "-f",
1112
+ challengeFile,
1113
+ "-r",
1114
+ resourceUrl,
1115
+ "-s",
1116
+ this.sessionId,
1117
+ "-i",
1118
+ intentSummary,
1119
+ "-w",
1120
+ this.framework
1121
+ ];
1122
+ if (opts.method) payArgs.push("-m", opts.method);
1123
+ if (opts.data) payArgs.push("-d", opts.data);
1124
+ const payRun = await this.runner(payArgs, { onLine, signal, step: "402-buyer-pay", flow });
1125
+ const tradeNo = parseTradeNo(payRun.lines);
1126
+ if (!tradeNo) {
1127
+ throw new AlipayProtocolError("402-buyer-pay did not return a tradeNo");
1128
+ }
1129
+ assertTradeNo(tradeNo);
1130
+ const { paymentUrl, shortenUrl } = parsePaymentUrl(payRun.lines);
1131
+ if (paymentUrl) {
1132
+ alipayLog.info("flow.pending", { flow, tradeNo, ms: this.now() - flowStart });
1133
+ opts.onPaymentPending?.({ paymentUrl, shortenUrl, tradeNo });
1134
+ }
1135
+ const pendingAt = this.now();
1136
+ const windowMs = (requirement.maxTimeoutSeconds ?? 30 * 60) * 1e3;
1137
+ const deadline = this.now() + (opts.timeoutMs ?? windowMs);
1138
+ const poll = await pollUntil(tradeNo, resourceUrl, {
1139
+ // No onLine: the status-poll output embeds the resource and must not reach
1140
+ // the log stream — the body is surfaced via the return value below (§9.3).
1141
+ deadline,
1142
+ signal,
1143
+ runner: this.runner,
1144
+ now: this.now,
1145
+ // Re-fetch the resource the same way it was paid (POST + body), else the
1146
+ // status poll defaults to GET and 404s on a POST-only `/execute`.
1147
+ method: opts.method,
1148
+ data: opts.data
1149
+ });
1150
+ alipayLog.info("flow.settled", { flow, tradeNo, ms: this.now() - pendingAt });
1151
+ const body = extractBody(poll.lines);
1152
+ void this.runner(["402-buyer-fulfillment-ack", "-t", tradeNo], {
1153
+ onLine,
1154
+ signal,
1155
+ step: "402-buyer-fulfillment-ack",
1156
+ flow
1157
+ }).catch(() => void 0);
1158
+ return { body, payment: { tradeNo, outTradeNo: reqId }, media };
1159
+ }
1160
+ };
1161
+
1162
+ // src/client/wechat/index.ts
1163
+ import { randomUUID as randomUUID2 } from "crypto";
1164
+ import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1165
+ import { homedir as homedir3 } from "os";
1166
+ import { join as join3 } from "path";
1167
+ var WECHAT_SCHEME = "wechatpay-native";
1168
+ var WECHAT_NETWORK = "wechat";
1169
+ var POLL_INTERVAL_MS2 = 3e3;
1170
+ var TIMEOUT_MS = 5 * 60 * 1e3;
1171
+ var WechatClient = class {
1172
+ configDir;
1173
+ sessionDir;
1174
+ now;
1175
+ constructor(options = {}) {
1176
+ this.configDir = options.configDir || join3(homedir3(), ".moltspay");
1177
+ this.sessionDir = join3(this.configDir, "wechat-sessions");
1178
+ this.now = options.now ?? Date.now;
1179
+ }
1180
+ async pay402(opts) {
1181
+ const session = this.start402({ ...opts });
1182
+ const result = await this.pollSession(session.paymentSessionId, {
1183
+ pollIntervalMs: opts.pollIntervalMs,
1184
+ timeoutMs: opts.timeoutMs,
1185
+ signal: opts.signal
1186
+ });
1187
+ if (result.status === "paid" || result.status === "completed") {
1188
+ return {
1189
+ body: result.resultBody ?? "",
1190
+ status: result.lastHttpStatus ?? 200
1191
+ };
1192
+ }
1193
+ throw new Error(result.lastError || `WeChat payment ended with status ${result.status}`);
1194
+ }
1195
+ /**
1196
+ * Start a recoverable WeChat payment session. This returns immediately after
1197
+ * persisting `out_trade_no`, QR payload, original request body, and context.
1198
+ */
1199
+ start402(opts) {
1200
+ const extra = opts.requirement.extra ?? {};
1201
+ const codeUrl = typeof extra.code_url === "string" ? extra.code_url : "";
1202
+ const outTradeNo = typeof extra.out_trade_no === "string" ? extra.out_trade_no : "";
1203
+ if (!codeUrl || !outTradeNo) {
1204
+ throw new Error(
1205
+ "WechatClient.pay402: wechatpay-native requirement is missing extra.code_url / extra.out_trade_no"
1206
+ );
1207
+ }
1208
+ const now = new Date(this.now());
1209
+ const timeoutMs = opts.timeoutMs ?? (opts.requirement.maxTimeoutSeconds ?? TIMEOUT_MS / 1e3) * 1e3;
1210
+ const session = {
1211
+ paymentSessionId: opts.paymentSessionId ?? `mpay_sess_${randomUUID2()}`,
1212
+ status: "pending",
1213
+ resourceUrl: opts.resourceUrl,
1214
+ method: opts.method ?? "POST",
1215
+ data: opts.data,
1216
+ requirement: opts.requirement,
1217
+ codeUrl,
1218
+ outTradeNo,
1219
+ createdAt: now.toISOString(),
1220
+ updatedAt: now.toISOString(),
1221
+ expiresAt: new Date(this.now() + timeoutMs).toISOString(),
1222
+ context: opts.context
1223
+ };
1224
+ this.saveSession(session);
1225
+ opts.onPaymentPending?.({ codeUrl, outTradeNo });
1226
+ return session;
1227
+ }
1228
+ /**
1229
+ * Query a persisted session once by replaying the original request with the
1230
+ * stored `out_trade_no` proof. 200 means paid + fulfilled; 402 means pending.
1231
+ */
1232
+ async status(identifier) {
1233
+ const session = this.loadSession(identifier);
1234
+ if (session.status === "cancelled" || session.status === "completed") return session;
1235
+ if (this.now() >= new Date(session.expiresAt).getTime()) {
1236
+ return this.updateSession(session, {
1237
+ status: "expired",
1238
+ lastError: `WeChat payment expired at ${session.expiresAt}`
1239
+ });
1240
+ }
1241
+ const xPayment = this.buildPaymentHeader(session);
1242
+ const res = await fetch(session.resourceUrl, {
1243
+ method: session.method,
1244
+ headers: { "Content-Type": "application/json", "X-Payment": xPayment },
1245
+ body: session.method === "POST" ? session.data : void 0
1246
+ });
1247
+ const text = await res.text().catch(() => "");
1248
+ if (res.status === 200) {
1249
+ return this.updateSession(session, {
1250
+ status: "completed",
1251
+ lastHttpStatus: res.status,
1252
+ lastError: void 0,
1253
+ resultBody: text
1254
+ });
1255
+ }
1256
+ if (res.status === 402) {
1257
+ return this.updateSession(session, {
1258
+ status: "pending",
1259
+ lastHttpStatus: res.status,
1260
+ lastError: text.slice(0, 500) || "wechat payment pending"
1261
+ });
1262
+ }
1263
+ return this.updateSession(session, {
1264
+ status: "failed",
1265
+ lastHttpStatus: res.status,
1266
+ lastError: `WeChat /execute returned HTTP ${res.status}: ${text.slice(0, 500)}`
1267
+ });
1268
+ }
1269
+ /** Poll a session until it completes, expires, fails, or is aborted. */
1270
+ async pollSession(identifier, opts = {}) {
1271
+ const first = this.loadSession(identifier);
1272
+ const deadline = Math.min(
1273
+ new Date(first.expiresAt).getTime(),
1274
+ this.now() + (opts.timeoutMs ?? TIMEOUT_MS)
1275
+ );
1276
+ const interval = opts.pollIntervalMs ?? POLL_INTERVAL_MS2;
1277
+ let session = first;
1278
+ while (this.now() < deadline) {
1279
+ if (opts.signal?.aborted) throw new Error("WeChat payment aborted");
1280
+ session = await this.status(session.paymentSessionId);
1281
+ if (session.status !== "pending") return session;
1282
+ await new Promise((r) => setTimeout(r, interval));
1283
+ }
1284
+ return this.updateSession(session, {
1285
+ status: "expired",
1286
+ lastError: `WeChat payment timed out after ${Math.round((opts.timeoutMs ?? TIMEOUT_MS) / 1e3)}s (out_trade_no=${session.outTradeNo}, last status ${session.lastHttpStatus ?? 0})`
1287
+ });
1288
+ }
1289
+ /** `fulfill` is an idempotent status check: paid sessions return stored body. */
1290
+ async fulfill(identifier) {
1291
+ const session = this.loadSession(identifier);
1292
+ if (session.status === "completed") return session;
1293
+ return this.status(identifier);
1294
+ }
1295
+ /** Mark a local session cancelled. Closing the merchant order is server-side. */
1296
+ cancel(identifier) {
1297
+ const session = this.loadSession(identifier);
1298
+ return this.updateSession(session, {
1299
+ status: "cancelled",
1300
+ lastError: "cancelled locally"
1301
+ });
1302
+ }
1303
+ listSessions() {
1304
+ this.ensureSessionDir();
1305
+ return readdirSync(this.sessionDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(readFileSync2(join3(this.sessionDir, name), "utf-8"))).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
1306
+ }
1307
+ buildPaymentHeader(session) {
1308
+ const proof = {
1309
+ x402Version: 2,
1310
+ scheme: WECHAT_SCHEME,
1311
+ network: WECHAT_NETWORK,
1312
+ accepted: {
1313
+ scheme: WECHAT_SCHEME,
1314
+ network: WECHAT_NETWORK,
1315
+ extra: { out_trade_no: session.outTradeNo }
1316
+ },
1317
+ payload: { out_trade_no: session.outTradeNo }
1318
+ };
1319
+ return Buffer.from(JSON.stringify(proof)).toString("base64");
1320
+ }
1321
+ loadSession(identifier) {
1322
+ this.ensureSessionDir();
1323
+ const direct = join3(this.sessionDir, `${identifier}.json`);
1324
+ try {
1325
+ return JSON.parse(readFileSync2(direct, "utf-8"));
1326
+ } catch {
1327
+ const byTradeNo = this.listSessions().find((s) => s.outTradeNo === identifier);
1328
+ if (byTradeNo) return byTradeNo;
1329
+ throw new Error(`WeChat payment session not found: ${identifier}`);
1330
+ }
1331
+ }
1332
+ saveSession(session) {
1333
+ this.ensureSessionDir();
1334
+ writeFileSync2(
1335
+ join3(this.sessionDir, `${session.paymentSessionId}.json`),
1336
+ JSON.stringify(session, null, 2)
1337
+ );
1338
+ }
1339
+ updateSession(session, updates) {
1340
+ const next = {
1341
+ ...session,
1342
+ ...updates,
1343
+ updatedAt: new Date(this.now()).toISOString()
1344
+ };
1345
+ this.saveSession(next);
1346
+ return next;
1347
+ }
1348
+ ensureSessionDir() {
1349
+ mkdirSync2(this.sessionDir, { recursive: true });
1350
+ }
1351
+ };
1352
+
1353
+ // src/client/alipay/router.ts
1354
+ var ALIPAY_RAIL = "alipay";
1355
+ var WECHAT_RAIL = "wechat";
1356
+ var BALANCE_RAIL = "balance";
1357
+ function railOf(req) {
1358
+ if (req.scheme === "alipay-aipay" || req.network === ALIPAY_RAIL) return ALIPAY_RAIL;
1359
+ if (req.scheme === "wechatpay-native" || req.network === WECHAT_RAIL) return WECHAT_RAIL;
1360
+ if (req.scheme === BALANCE_RAIL || req.network === BALANCE_RAIL) return BALANCE_RAIL;
1361
+ return networkToChainName(req.network) ?? req.network;
1362
+ }
1363
+ function findRail(accepts, rail) {
1364
+ const requirement = accepts.find((r) => railOf(r) === rail);
1365
+ return requirement ? { rail, requirement } : null;
1366
+ }
1367
+ function selectRail(input) {
1368
+ const { serverAccepts, explicitRail, preference, availability } = input;
1369
+ if (!serverAccepts || serverAccepts.length === 0) {
1370
+ throw new UnsupportedRailError(explicitRail ?? "unknown", "Server offered no payment options");
1371
+ }
1372
+ if (explicitRail) {
1373
+ const hit = findRail(serverAccepts, explicitRail);
1374
+ if (!hit) {
1375
+ const offered = serverAccepts.map(railOf);
1376
+ throw new UnsupportedRailError(
1377
+ explicitRail,
1378
+ `Server doesn't accept rail '${explicitRail}'. Offered: ${offered.join(", ")}`
1379
+ );
1380
+ }
1381
+ return hit;
1382
+ }
1383
+ if (preference) {
1384
+ for (const pref of preference) {
1385
+ const hit = findRail(serverAccepts, pref);
1386
+ if (hit) return hit;
1387
+ }
1388
+ }
1389
+ if (availability?.evmReady) {
1390
+ const evm = serverAccepts.find((r) => railOf(r) !== ALIPAY_RAIL);
1391
+ if (evm) return { rail: railOf(evm), requirement: evm };
1392
+ }
1393
+ if (availability?.alipayReady) {
1394
+ const hit = findRail(serverAccepts, ALIPAY_RAIL);
1395
+ if (hit) return hit;
1396
+ }
1397
+ return { rail: railOf(serverAccepts[0]), requirement: serverAccepts[0] };
1398
+ }
1399
+
1400
+ // src/facilitators/balance/auth.ts
1401
+ import { ethers as ethers2 } from "ethers";
1402
+ var BALANCE_AUTH_DOMAIN = "moltspay-balance-auth:v1";
1403
+ var BALANCE_AUTH_MAX_SKEW_MS = 5 * 60 * 1e3;
1404
+ function buildDeductMessage(f) {
1405
+ return [BALANCE_AUTH_DOMAIN, "balance-deduct", f.buyerId, f.requestId, f.service, String(f.timestamp)].join("\n");
1406
+ }
1407
+
467
1408
  // src/client/node/index.ts
468
1409
  var DEFAULT_CONFIG = {
469
1410
  chain: "base",
@@ -480,9 +1421,13 @@ var MoltsPayClient = class {
480
1421
  signer = null;
481
1422
  todaySpending = 0;
482
1423
  lastSpendingReset = 0;
1424
+ railPreference;
1425
+ alipaySessionId;
483
1426
  constructor(options = {}) {
484
- this.configDir = options.configDir || join2(homedir2(), ".moltspay");
1427
+ this.configDir = options.configDir || join4(homedir4(), ".moltspay");
485
1428
  this.config = this.loadConfig();
1429
+ this.railPreference = options.railPreference ?? this.config.railPreference;
1430
+ this.alipaySessionId = options.alipaySessionId;
486
1431
  this.walletData = this.loadWallet();
487
1432
  this.loadSpending();
488
1433
  if (this.walletData) {
@@ -560,6 +1505,15 @@ var MoltsPayClient = class {
560
1505
  * @param options - Payment options (token selection)
561
1506
  */
562
1507
  async pay(serverUrl, service, params, options = {}) {
1508
+ if (options.rail === ALIPAY_RAIL) {
1509
+ return this.payViaAlipay(serverUrl, service, params, options);
1510
+ }
1511
+ if (options.rail === WECHAT_RAIL) {
1512
+ return this.payViaWechat(serverUrl, service, params, options);
1513
+ }
1514
+ if (options.rail === BALANCE_RAIL) {
1515
+ return this.payViaBalance(serverUrl, service, params, options);
1516
+ }
563
1517
  if (!this.wallet || !this.walletData) {
564
1518
  throw new Error("Client not initialized. Run: npx moltspay init");
565
1519
  }
@@ -751,6 +1705,469 @@ Please specify: --chain <chain_name>`
751
1705
  console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
752
1706
  return result.result || result;
753
1707
  }
1708
+ /**
1709
+ * Pay for a service over the Alipay fiat rail (2.0.0).
1710
+ *
1711
+ * Unlike the crypto path this needs no EVM wallet — it shells out to
1712
+ * alipay-bot via {@link AlipayClient}. Flow: hit the resource with no
1713
+ * payment to get the 402 challenge, confirm the server actually offers the
1714
+ * alipay rail (selectRail), then run the 8-step state machine and return the
1715
+ * resource body.
1716
+ */
1717
+ async payViaAlipay(serverUrl, service, params, options) {
1718
+ const flow = this.alipaySessionId;
1719
+ let executeUrl = `${serverUrl}/execute`;
1720
+ try {
1721
+ const services = await timeStep(
1722
+ "discover-services",
1723
+ flow,
1724
+ () => this.getServices(serverUrl)
1725
+ );
1726
+ const svc = services.services?.find((s) => s.id === service);
1727
+ if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
1728
+ } catch {
1729
+ }
1730
+ const requestBody = options.rawData ? { service, ...params } : { service, params };
1731
+ const bodyJson = JSON.stringify(requestBody);
1732
+ const res = await timeStep(
1733
+ "challenge-402",
1734
+ flow,
1735
+ () => fetch(executeUrl, {
1736
+ method: "POST",
1737
+ headers: { "Content-Type": "application/json" },
1738
+ body: bodyJson
1739
+ })
1740
+ );
1741
+ if (res.status !== 402) {
1742
+ const data = await res.json().catch(() => ({}));
1743
+ if (res.ok && data.result) return data.result;
1744
+ throw new Error(data.error || `Expected 402, got ${res.status}`);
1745
+ }
1746
+ const header = res.headers.get(PAYMENT_REQUIRED_HEADER);
1747
+ if (!header) throw new Error("Missing x-payment-required header on 402");
1748
+ const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
1749
+ const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
1750
+ const { requirement } = selectRail({
1751
+ serverAccepts: accepts,
1752
+ explicitRail: ALIPAY_RAIL,
1753
+ preference: this.railPreference,
1754
+ availability: { evmReady: this.isInitialized }
1755
+ });
1756
+ const onLine = options.onLine ?? ((line) => process.stdout.write(line + "\n"));
1757
+ const alipay = new AlipayClient({
1758
+ sessionId: this.alipaySessionId,
1759
+ configDir: this.configDir
1760
+ });
1761
+ const result = await alipay.pay402({
1762
+ resourceUrl: executeUrl,
1763
+ requirement,
1764
+ method: "POST",
1765
+ data: bodyJson,
1766
+ onLine,
1767
+ onPaymentPending: options.onPaymentPending,
1768
+ timeoutMs: options.timeoutMs,
1769
+ signal: options.signal
1770
+ });
1771
+ try {
1772
+ const json = JSON.parse(result.body);
1773
+ return json.result ?? json;
1774
+ } catch {
1775
+ return { body: result.body, payment: result.payment, media: result.media };
1776
+ }
1777
+ }
1778
+ /**
1779
+ * Pay for a service over the WeChat Pay Native fiat rail (2.1.0).
1780
+ *
1781
+ * Mirrors {@link payViaAlipay} and needs no EVM wallet. The server placed the
1782
+ * Native order when it built the 402, so its `wechatpay-native` accepts[]
1783
+ * entry already carries `extra.code_url` + `extra.out_trade_no`. Flow: hit the
1784
+ * resource to get the 402, confirm the server offers the wechat rail, surface
1785
+ * the code_url (caller renders a QR), then poll the resource with the
1786
+ * out_trade_no proof until the server verifies the order paid and delivers.
1787
+ */
1788
+ async payViaWechat(serverUrl, service, params, options) {
1789
+ const session = await this.startWechatPayment(serverUrl, service, params, options);
1790
+ const wechat = new WechatClient({ configDir: this.configDir });
1791
+ const result = await wechat.pollSession(session.paymentSessionId, {
1792
+ timeoutMs: options.timeoutMs,
1793
+ signal: options.signal
1794
+ });
1795
+ if (result.status !== "completed") {
1796
+ throw new Error(result.lastError || `WeChat payment ended with status ${result.status}`);
1797
+ }
1798
+ try {
1799
+ const json = JSON.parse(result.resultBody ?? "");
1800
+ return json.result ?? json;
1801
+ } catch {
1802
+ return { body: result.resultBody ?? "" };
1803
+ }
1804
+ }
1805
+ /**
1806
+ * The ethers wallet used to sign balance-rail deductions: the client's EVM
1807
+ * wallet if it has one, else a per-configDir identity key persisted at
1808
+ * `<configDir>/balance-identity.key` (0600) so a balance-only client works
1809
+ * without a full crypto wallet. Under `agent 统一代付`, one key spends every
1810
+ * account the agent tops up.
1811
+ */
1812
+ balanceSigner() {
1813
+ if (this.wallet) return this.wallet;
1814
+ const p = join4(this.configDir, "balance-identity.key");
1815
+ let pk;
1816
+ if (existsSync2(p)) {
1817
+ pk = readFileSync3(p, "utf-8").trim();
1818
+ } else {
1819
+ mkdirSync3(this.configDir, { recursive: true });
1820
+ pk = Wallet2.createRandom().privateKey;
1821
+ writeFileSync3(p, pk, { mode: 384 });
1822
+ }
1823
+ return new Wallet2(pk);
1824
+ }
1825
+ /** The balance-rail spending signer address (lowercase 0x…). Stable per
1826
+ * configDir; this is the identity the server TOFU-binds and later verifies. */
1827
+ getBalanceSignerAddress() {
1828
+ return this.balanceSigner().address.toLowerCase();
1829
+ }
1830
+ /** The buyer id for the balance rail: explicit option > persisted config. */
1831
+ resolveBuyerId(explicit) {
1832
+ const buyerId = explicit ?? this.config.buyerId;
1833
+ if (!buyerId) {
1834
+ throw new Error(
1835
+ "Balance rail needs a buyer id. Pass { buyerId } or persist one with setBuyerId()."
1836
+ );
1837
+ }
1838
+ return buyerId;
1839
+ }
1840
+ /**
1841
+ * Pay via the custodial balance rail (2.2.0, password-free).
1842
+ *
1843
+ * No wallet, no QR: the request carries `{buyer_id, request_id}` in the
1844
+ * X-Payment payload and the server deducts the prepaid balance atomically
1845
+ * before running the skill. The client-generated `request_id` makes the
1846
+ * charge idempotent — a network retry can never double-deduct.
1847
+ */
1848
+ async payViaBalance(serverUrl, service, params, options) {
1849
+ const buyerId = this.resolveBuyerId(options.buyerId);
1850
+ let attempt = await this.balanceDeduct(serverUrl, service, params, options, buyerId);
1851
+ if (attempt.ok) return attempt.result;
1852
+ const fundable = attempt.status === 402 && (attempt.code === "buyer_not_found" || attempt.code === "insufficient_balance" || /insufficient balance|unknown buyer|top up first/i.test(attempt.error || ""));
1853
+ if (!fundable || options.autoTopup === false) {
1854
+ throw new Error(attempt.error || `Balance payment failed with HTTP ${attempt.status}`);
1855
+ }
1856
+ if (options.topupMode === "manual") {
1857
+ const order = await this.createBalanceTopupOrder(serverUrl, {
1858
+ pack: options.topupPack,
1859
+ buyerId,
1860
+ context: { service }
1861
+ });
1862
+ options.onTopupRequired?.(order.pack, order.codeUrl);
1863
+ return {
1864
+ status: "topup_required",
1865
+ out_trade_no: order.outTradeNo,
1866
+ code_url: order.codeUrl,
1867
+ pack: order.pack,
1868
+ server_url: serverUrl
1869
+ };
1870
+ }
1871
+ const credited = await this.topupBalancePack(serverUrl, {
1872
+ pack: options.topupPack,
1873
+ buyerId,
1874
+ pollIntervalMs: options.topupPollIntervalMs,
1875
+ signal: options.signal,
1876
+ onCodeUrl: (pack, codeUrl) => options.onTopupRequired?.(pack, codeUrl)
1877
+ });
1878
+ options.onTopupCredited?.(credited.balance);
1879
+ attempt = await this.balanceDeduct(serverUrl, service, params, options, buyerId);
1880
+ if (attempt.ok) return attempt.result;
1881
+ throw new Error(attempt.error || "Balance payment failed after top-up");
1882
+ }
1883
+ /** One password-free deduct attempt. Never throws on an HTTP error; the
1884
+ * caller inspects `{ ok, status, error }` to decide whether to auto-fund. */
1885
+ async balanceDeduct(serverUrl, service, params, options, buyerId) {
1886
+ let executeUrl = `${serverUrl}/execute`;
1887
+ try {
1888
+ const services = await this.getServices(serverUrl);
1889
+ const svc = services.services?.find((s) => s.id === service);
1890
+ if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
1891
+ } catch {
1892
+ }
1893
+ const requestBody = options.rawData ? { service, ...params } : { service, params };
1894
+ const requestId = randomUUID3();
1895
+ const timestamp = Math.floor(Date.now() / 1e3);
1896
+ const signature = await this.balanceSigner().signMessage(
1897
+ buildDeductMessage({ buyerId, requestId, service, timestamp })
1898
+ );
1899
+ const xPayment = Buffer.from(JSON.stringify({
1900
+ x402Version: 2,
1901
+ scheme: BALANCE_RAIL,
1902
+ network: BALANCE_RAIL,
1903
+ payload: { buyer_id: buyerId, request_id: requestId, auth: { timestamp, signature } }
1904
+ })).toString("base64");
1905
+ const res = await fetch(executeUrl, {
1906
+ method: "POST",
1907
+ headers: { "Content-Type": "application/json", "X-Payment": xPayment },
1908
+ body: JSON.stringify(requestBody),
1909
+ signal: options.signal
1910
+ });
1911
+ const data = await res.json().catch(() => ({}));
1912
+ if (!res.ok) return { ok: false, status: res.status, error: data.error, code: data.code };
1913
+ return { ok: true, status: res.status, result: data.result ?? data };
1914
+ }
1915
+ /**
1916
+ * Non-blocking: POST /balance/topup/order, persist a recoverable session, and
1917
+ * return at once (no polling). Use with {@link confirmBalanceTopup} for
1918
+ * turn-based agents; the blocking {@link topupBalancePack} is built on this.
1919
+ */
1920
+ async createBalanceTopupOrder(serverUrl, opts = {}) {
1921
+ const id = this.resolveBuyerId(opts.buyerId);
1922
+ const orderRes = await fetch(`${serverUrl}/balance/topup/order`, {
1923
+ method: "POST",
1924
+ headers: { "Content-Type": "application/json" },
1925
+ // Carry the spending signer so the server binds it to the account on
1926
+ // confirm (topup-time binding). The same key signs deductions later.
1927
+ body: JSON.stringify({ buyer_id: id, pack: opts.pack, signer_address: this.getBalanceSignerAddress() })
1928
+ });
1929
+ const order = await orderRes.json().catch(() => ({}));
1930
+ if (!orderRes.ok) throw new Error(order.error || `Top-up order failed with HTTP ${orderRes.status}`);
1931
+ const maxTimeoutSeconds = order.max_timeout_seconds ?? 300;
1932
+ const now = Date.now();
1933
+ this.saveBalanceTopupSession({
1934
+ out_trade_no: order.out_trade_no,
1935
+ buyer_id: id,
1936
+ pack: order.pack,
1937
+ server_url: serverUrl,
1938
+ code_url: order.code_url,
1939
+ status: "pending",
1940
+ created_at: new Date(now).toISOString(),
1941
+ expires_at: new Date(now + maxTimeoutSeconds * 1e3).toISOString(),
1942
+ context: opts.context
1943
+ });
1944
+ return { outTradeNo: order.out_trade_no, codeUrl: order.code_url, pack: order.pack, maxTimeoutSeconds };
1945
+ }
1946
+ /**
1947
+ * One-shot: POST /balance/topup/confirm for a single order. No polling.
1948
+ * Updates the persisted session on credit. `serverUrl` defaults to the one
1949
+ * recorded in the session (recover by out_trade_no alone).
1950
+ */
1951
+ async confirmBalanceTopup(outTradeNo, opts = {}) {
1952
+ const session = this.getBalanceTopupSession(outTradeNo);
1953
+ const serverUrl = opts.serverUrl || session?.server_url;
1954
+ if (!serverUrl) {
1955
+ return { credited: false, reason: `No server URL for ${outTradeNo}: pass serverUrl or run topup-order first` };
1956
+ }
1957
+ const confRes = await fetch(`${serverUrl}/balance/topup/confirm`, {
1958
+ method: "POST",
1959
+ headers: { "Content-Type": "application/json" },
1960
+ body: JSON.stringify({ out_trade_no: outTradeNo })
1961
+ });
1962
+ const conf = await confRes.json().catch(() => ({}));
1963
+ if (!confRes.ok) return { credited: false, reason: conf.error || `Confirm failed with HTTP ${confRes.status}` };
1964
+ if (conf.credited) {
1965
+ if (session) {
1966
+ session.status = "credited";
1967
+ session.tx_id = conf.tx_id;
1968
+ session.balance = conf.balance;
1969
+ this.saveBalanceTopupSession(session);
1970
+ }
1971
+ return { credited: true, balance: conf.balance, txId: conf.tx_id };
1972
+ }
1973
+ return { credited: false, pending: !!conf.pending, reason: conf.reason };
1974
+ }
1975
+ /**
1976
+ * Blocking terminal wrapper: create the order then poll confirm until the
1977
+ * scan is credited (or the order expires). Built on
1978
+ * {@link createBalanceTopupOrder} + {@link confirmBalanceTopup}.
1979
+ */
1980
+ async topupBalancePack(serverUrl, opts = {}) {
1981
+ const order = await this.createBalanceTopupOrder(serverUrl, { pack: opts.pack, buyerId: opts.buyerId });
1982
+ opts.onCodeUrl?.(order.pack, order.codeUrl);
1983
+ const interval = opts.pollIntervalMs ?? 2e3;
1984
+ const deadline = Date.now() + order.maxTimeoutSeconds * 1e3;
1985
+ while (Date.now() < deadline) {
1986
+ if (opts.signal?.aborted) throw new Error("Top-up aborted");
1987
+ const conf = await this.confirmBalanceTopup(order.outTradeNo, { serverUrl });
1988
+ if (conf.credited) return { balance: conf.balance, outTradeNo: order.outTradeNo, txId: conf.txId };
1989
+ await this.sleep(interval, opts.signal);
1990
+ }
1991
+ throw new Error("Top-up timed out before the payment was confirmed");
1992
+ }
1993
+ // --- Recoverable balance top-up sessions (<configDir>/balance-topup-sessions) ---
1994
+ balanceTopupSessionDir() {
1995
+ return join4(this.configDir, "balance-topup-sessions");
1996
+ }
1997
+ saveBalanceTopupSession(session) {
1998
+ const dir = this.balanceTopupSessionDir();
1999
+ mkdirSync3(dir, { recursive: true });
2000
+ writeFileSync3(join4(dir, `${session.out_trade_no}.json`), JSON.stringify(session, null, 2));
2001
+ }
2002
+ /** Read a persisted top-up session by out_trade_no, or null. */
2003
+ getBalanceTopupSession(outTradeNo) {
2004
+ const p = join4(this.balanceTopupSessionDir(), `${outTradeNo}.json`);
2005
+ if (!existsSync2(p)) return null;
2006
+ try {
2007
+ return JSON.parse(readFileSync3(p, "utf-8"));
2008
+ } catch {
2009
+ return null;
2010
+ }
2011
+ }
2012
+ /** List persisted top-up sessions, newest first. */
2013
+ listBalanceTopupSessions() {
2014
+ const dir = this.balanceTopupSessionDir();
2015
+ if (!existsSync2(dir)) return [];
2016
+ return readdirSync2(dir).filter((f) => f.endsWith(".json")).map((f) => {
2017
+ try {
2018
+ return JSON.parse(readFileSync3(join4(dir, f), "utf-8"));
2019
+ } catch {
2020
+ return null;
2021
+ }
2022
+ }).filter((s) => s !== null).sort((a, b) => (b.created_at || "").localeCompare(a.created_at || ""));
2023
+ }
2024
+ /** Abortable sleep used by the top-up poll loop. */
2025
+ sleep(ms, signal) {
2026
+ return new Promise((resolve, reject) => {
2027
+ if (signal?.aborted) return reject(new Error("aborted"));
2028
+ const t = setTimeout(resolve, ms);
2029
+ signal?.addEventListener("abort", () => {
2030
+ clearTimeout(t);
2031
+ reject(new Error("aborted"));
2032
+ }, { once: true });
2033
+ });
2034
+ }
2035
+ /** Persist the buyer id used by the balance rail (bearer semantics). */
2036
+ setBuyerId(buyerId) {
2037
+ this.config.buyerId = buyerId;
2038
+ this.saveConfig();
2039
+ }
2040
+ /** GET /balance — custodial balance, limits, and today's spend for a buyer. */
2041
+ async getBuyerBalance(serverUrl, buyerId) {
2042
+ const id = this.resolveBuyerId(buyerId);
2043
+ const res = await fetch(`${serverUrl}/balance?buyer_id=${encodeURIComponent(id)}`);
2044
+ const data = await res.json().catch(() => ({}));
2045
+ if (!res.ok) throw new Error(data.error || `Balance query failed with HTTP ${res.status}`);
2046
+ return data;
2047
+ }
2048
+ /**
2049
+ * POST /balance/topup — report an externally settled payment (on-chain
2050
+ * tx hash / Alipay trade_no / WeChat out_trade_no) so the server verifies
2051
+ * and credits the ledger. Idempotent per reference.
2052
+ */
2053
+ async topupBalance(serverUrl, opts) {
2054
+ const id = this.resolveBuyerId(opts.buyerId);
2055
+ const res = await fetch(`${serverUrl}/balance/topup`, {
2056
+ method: "POST",
2057
+ headers: { "Content-Type": "application/json" },
2058
+ body: JSON.stringify({
2059
+ buyer_id: id,
2060
+ rail: opts.rail,
2061
+ amount: opts.amount,
2062
+ tx_hash: opts.txHash,
2063
+ chain: opts.chain,
2064
+ trade_no: opts.tradeNo,
2065
+ out_trade_no: opts.outTradeNo
2066
+ })
2067
+ });
2068
+ const data = await res.json().catch(() => ({}));
2069
+ if (!res.ok) throw new Error(data.error || `Top-up failed with HTTP ${res.status}`);
2070
+ return data;
2071
+ }
2072
+ /** GET /balance/transactions — ledger history for a buyer, newest first. */
2073
+ async listBalanceTransactions(serverUrl, opts = {}) {
2074
+ const id = this.resolveBuyerId(opts.buyerId);
2075
+ const qs = new URLSearchParams({ buyer_id: id });
2076
+ if (opts.limit) qs.set("limit", String(opts.limit));
2077
+ if (opts.offset) qs.set("offset", String(opts.offset));
2078
+ const res = await fetch(`${serverUrl}/balance/transactions?${qs}`);
2079
+ const data = await res.json().catch(() => ({}));
2080
+ if (!res.ok) throw new Error(data.error || `Transaction query failed with HTTP ${res.status}`);
2081
+ return data;
2082
+ }
2083
+ /**
2084
+ * Start a recoverable WeChat Pay Native session and return immediately with
2085
+ * QR metadata. The SDK client persists enough context to poll/fulfill later.
2086
+ */
2087
+ async startWechatPayment(serverUrl, service, params, options = {}) {
2088
+ let executeUrl = `${serverUrl}/execute`;
2089
+ try {
2090
+ const services = await this.getServices(serverUrl);
2091
+ const svc = services.services?.find((s) => s.id === service);
2092
+ if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
2093
+ } catch {
2094
+ }
2095
+ const requestBody = options.rawData ? { service, ...params } : { service, params };
2096
+ const bodyJson = JSON.stringify(requestBody);
2097
+ const res = await fetch(executeUrl, {
2098
+ method: "POST",
2099
+ headers: { "Content-Type": "application/json" },
2100
+ body: bodyJson
2101
+ });
2102
+ if (res.status !== 402) {
2103
+ const data = await res.json().catch(() => ({}));
2104
+ if (res.ok && data.result) return data.result;
2105
+ throw new Error(data.error || `Expected 402, got ${res.status}`);
2106
+ }
2107
+ const header = res.headers.get(PAYMENT_REQUIRED_HEADER);
2108
+ if (!header) throw new Error("Missing x-payment-required header on 402");
2109
+ const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
2110
+ const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
2111
+ const { requirement } = selectRail({
2112
+ serverAccepts: accepts,
2113
+ explicitRail: WECHAT_RAIL,
2114
+ preference: this.railPreference,
2115
+ availability: { evmReady: this.isInitialized }
2116
+ });
2117
+ const wechat = new WechatClient({ configDir: this.configDir });
2118
+ const session = wechat.start402({
2119
+ resourceUrl: executeUrl,
2120
+ requirement,
2121
+ method: "POST",
2122
+ data: bodyJson,
2123
+ onPaymentPending: options.onPaymentPending ? (info) => options.onPaymentPending({ paymentUrl: info.codeUrl, tradeNo: info.outTradeNo }) : void 0,
2124
+ timeoutMs: options.timeoutMs,
2125
+ signal: options.signal,
2126
+ context: {
2127
+ serverUrl,
2128
+ service,
2129
+ params,
2130
+ rawData: options.rawData ?? false,
2131
+ rail: WECHAT_RAIL
2132
+ }
2133
+ });
2134
+ if (options.autoPoll || options.onWechatPaymentCompleted || options.onWechatPaymentFailed) {
2135
+ void wechat.pollSession(session.paymentSessionId, {
2136
+ timeoutMs: options.timeoutMs,
2137
+ signal: options.signal
2138
+ }).then(async (finalSession) => {
2139
+ if (finalSession.status === "completed") {
2140
+ await options.onWechatPaymentCompleted?.(finalSession);
2141
+ } else {
2142
+ await options.onWechatPaymentFailed?.(finalSession);
2143
+ }
2144
+ }).catch(async (error) => {
2145
+ const failed = await wechat.fulfill(session.paymentSessionId).catch(() => ({
2146
+ ...session,
2147
+ status: "failed",
2148
+ lastError: error instanceof Error ? error.message : String(error)
2149
+ }));
2150
+ await options.onWechatPaymentFailed?.(failed);
2151
+ });
2152
+ }
2153
+ return session;
2154
+ }
2155
+ /** Query a persisted WeChat session once. */
2156
+ async getWechatPaymentStatus(identifier) {
2157
+ return new WechatClient({ configDir: this.configDir }).status(identifier);
2158
+ }
2159
+ /** Idempotently fulfill a paid WeChat session, returning stored or fetched result. */
2160
+ async fulfillWechatPayment(identifier) {
2161
+ return new WechatClient({ configDir: this.configDir }).fulfill(identifier);
2162
+ }
2163
+ /** Mark a local WeChat session as cancelled. */
2164
+ cancelWechatPayment(identifier) {
2165
+ return new WechatClient({ configDir: this.configDir }).cancel(identifier);
2166
+ }
2167
+ /** List persisted WeChat sessions, newest first. */
2168
+ listWechatPaymentSessions() {
2169
+ return new WechatClient({ configDir: this.configDir }).listSessions();
2170
+ }
754
2171
  /**
755
2172
  * Handle MPP (Machine Payments Protocol) payment flow
756
2173
  * Called when pay() detects WWW-Authenticate header in 402 response
@@ -846,14 +2263,14 @@ Please specify: --chain <chain_name>`
846
2263
  async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
847
2264
  const { to, amount, token, chainName, chain, spender } = paymentDetails;
848
2265
  const tokenConfig = chain.tokens[token];
849
- const provider = new ethers2.JsonRpcProvider(chain.rpc);
2266
+ const provider = new ethers3.JsonRpcProvider(chain.rpc);
850
2267
  const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
851
2268
  const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
852
2269
  if (allowance < amountWeiCheck) {
853
2270
  const nativeBalance = await provider.getBalance(this.wallet.address);
854
- const minGasBalance = ethers2.parseEther("0.0005");
2271
+ const minGasBalance = ethers3.parseEther("0.0005");
855
2272
  if (nativeBalance < minGasBalance) {
856
- const nativeBNB = parseFloat(ethers2.formatEther(nativeBalance)).toFixed(4);
2273
+ const nativeBNB = parseFloat(ethers3.formatEther(nativeBalance)).toFixed(4);
857
2274
  const isTestnet = chainName === "bnb_testnet";
858
2275
  if (isTestnet) {
859
2276
  throw new Error(
@@ -1026,7 +2443,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1026
2443
  * Check ERC20 allowance for a spender
1027
2444
  */
1028
2445
  async checkAllowance(tokenAddress, spender, provider) {
1029
- const contract = new ethers2.Contract(
2446
+ const contract = new ethers3.Contract(
1030
2447
  tokenAddress,
1031
2448
  ["function allowance(address owner, address spender) view returns (uint256)"],
1032
2449
  provider
@@ -1045,7 +2462,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1045
2462
  async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
1046
2463
  const tokenConfig = chain.tokens[token];
1047
2464
  const value = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
1048
- const nonce = ethers2.hexlify(ethers2.randomBytes(32));
2465
+ const nonce = ethers3.hexlify(ethers3.randomBytes(32));
1049
2466
  const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
1050
2467
  const tokenVersion = domainOverride?.version || "2";
1051
2468
  const envelope = buildEIP3009TypedData({
@@ -1091,26 +2508,26 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1091
2508
  }
1092
2509
  // --- Config & Wallet Management ---
1093
2510
  loadConfig() {
1094
- const configPath = join2(this.configDir, "config.json");
2511
+ const configPath = join4(this.configDir, "config.json");
1095
2512
  if (existsSync2(configPath)) {
1096
- const content = readFileSync2(configPath, "utf-8");
2513
+ const content = readFileSync3(configPath, "utf-8");
1097
2514
  return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
1098
2515
  }
1099
2516
  return { ...DEFAULT_CONFIG };
1100
2517
  }
1101
2518
  saveConfig() {
1102
- mkdirSync2(this.configDir, { recursive: true });
1103
- const configPath = join2(this.configDir, "config.json");
1104
- writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
2519
+ mkdirSync3(this.configDir, { recursive: true });
2520
+ const configPath = join4(this.configDir, "config.json");
2521
+ writeFileSync3(configPath, JSON.stringify(this.config, null, 2));
1105
2522
  }
1106
2523
  /**
1107
2524
  * Load spending data from disk
1108
2525
  */
1109
2526
  loadSpending() {
1110
- const spendingPath = join2(this.configDir, "spending.json");
2527
+ const spendingPath = join4(this.configDir, "spending.json");
1111
2528
  if (existsSync2(spendingPath)) {
1112
2529
  try {
1113
- const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
2530
+ const data = JSON.parse(readFileSync3(spendingPath, "utf-8"));
1114
2531
  const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
1115
2532
  if (data.date && data.date === today) {
1116
2533
  this.todaySpending = data.amount || 0;
@@ -1129,17 +2546,17 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1129
2546
  * Save spending data to disk
1130
2547
  */
1131
2548
  saveSpending() {
1132
- mkdirSync2(this.configDir, { recursive: true });
1133
- const spendingPath = join2(this.configDir, "spending.json");
2549
+ mkdirSync3(this.configDir, { recursive: true });
2550
+ const spendingPath = join4(this.configDir, "spending.json");
1134
2551
  const data = {
1135
2552
  date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
1136
2553
  amount: this.todaySpending,
1137
2554
  updatedAt: Date.now()
1138
2555
  };
1139
- writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
2556
+ writeFileSync3(spendingPath, JSON.stringify(data, null, 2));
1140
2557
  }
1141
2558
  loadWallet() {
1142
- const walletPath = join2(this.configDir, "wallet.json");
2559
+ const walletPath = join4(this.configDir, "wallet.json");
1143
2560
  if (existsSync2(walletPath)) {
1144
2561
  if (process.platform !== "win32") {
1145
2562
  try {
@@ -1153,7 +2570,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1153
2570
  } catch {
1154
2571
  }
1155
2572
  }
1156
- const content = readFileSync2(walletPath, "utf-8");
2573
+ const content = readFileSync3(walletPath, "utf-8");
1157
2574
  return JSON.parse(content);
1158
2575
  }
1159
2576
  return null;
@@ -1162,15 +2579,15 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1162
2579
  * Initialize a new wallet (called by CLI)
1163
2580
  */
1164
2581
  static init(configDir, options) {
1165
- mkdirSync2(configDir, { recursive: true });
2582
+ mkdirSync3(configDir, { recursive: true });
1166
2583
  const wallet = Wallet2.createRandom();
1167
2584
  const walletData = {
1168
2585
  address: wallet.address,
1169
2586
  privateKey: wallet.privateKey,
1170
2587
  createdAt: Date.now()
1171
2588
  };
1172
- const walletPath = join2(configDir, "wallet.json");
1173
- writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
2589
+ const walletPath = join4(configDir, "wallet.json");
2590
+ writeFileSync3(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
1174
2591
  const config = {
1175
2592
  chain: options.chain,
1176
2593
  limits: {
@@ -1178,8 +2595,8 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1178
2595
  maxPerDay: options.maxPerDay
1179
2596
  }
1180
2597
  };
1181
- const configPath = join2(configDir, "config.json");
1182
- writeFileSync2(configPath, JSON.stringify(config, null, 2));
2598
+ const configPath = join4(configDir, "config.json");
2599
+ writeFileSync3(configPath, JSON.stringify(config, null, 2));
1183
2600
  return { address: wallet.address, configDir };
1184
2601
  }
1185
2602
  /**
@@ -1195,17 +2612,17 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1195
2612
  } catch {
1196
2613
  throw new Error(`Unknown chain: ${this.config.chain}`);
1197
2614
  }
1198
- const provider = new ethers2.JsonRpcProvider(chain.rpc);
2615
+ const provider = new ethers3.JsonRpcProvider(chain.rpc);
1199
2616
  const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
1200
2617
  const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
1201
2618
  provider.getBalance(this.wallet.address),
1202
- new ethers2.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
1203
- new ethers2.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
2619
+ new ethers3.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
2620
+ new ethers3.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
1204
2621
  ]);
1205
2622
  return {
1206
- usdc: parseFloat(ethers2.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
1207
- usdt: parseFloat(ethers2.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
1208
- native: parseFloat(ethers2.formatEther(nativeBalance))
2623
+ usdc: parseFloat(ethers3.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
2624
+ usdt: parseFloat(ethers3.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
2625
+ native: parseFloat(ethers3.formatEther(nativeBalance))
1209
2626
  };
1210
2627
  }
1211
2628
  /**
@@ -1228,38 +2645,38 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1228
2645
  supportedChains.map(async (chainName) => {
1229
2646
  try {
1230
2647
  const chain = getChain(chainName);
1231
- const provider = new ethers2.JsonRpcProvider(chain.rpc);
2648
+ const provider = new ethers3.JsonRpcProvider(chain.rpc);
1232
2649
  if (chainName === "tempo_moderato") {
1233
2650
  const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
1234
2651
  provider.getBalance(this.wallet.address),
1235
- new ethers2.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1236
- new ethers2.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1237
- new ethers2.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
1238
- new ethers2.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
2652
+ new ethers3.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
2653
+ new ethers3.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
2654
+ new ethers3.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
2655
+ new ethers3.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
1239
2656
  ]);
1240
2657
  results[chainName] = {
1241
- usdc: parseFloat(ethers2.formatUnits(pathUSD, 6)),
2658
+ usdc: parseFloat(ethers3.formatUnits(pathUSD, 6)),
1242
2659
  // pathUSD as default USDC
1243
- usdt: parseFloat(ethers2.formatUnits(alphaUSD, 6)),
2660
+ usdt: parseFloat(ethers3.formatUnits(alphaUSD, 6)),
1244
2661
  // alphaUSD as default USDT
1245
- native: parseFloat(ethers2.formatEther(nativeBalance)),
2662
+ native: parseFloat(ethers3.formatEther(nativeBalance)),
1246
2663
  tempo: {
1247
- pathUSD: parseFloat(ethers2.formatUnits(pathUSD, 6)),
1248
- alphaUSD: parseFloat(ethers2.formatUnits(alphaUSD, 6)),
1249
- betaUSD: parseFloat(ethers2.formatUnits(betaUSD, 6)),
1250
- thetaUSD: parseFloat(ethers2.formatUnits(thetaUSD, 6))
2664
+ pathUSD: parseFloat(ethers3.formatUnits(pathUSD, 6)),
2665
+ alphaUSD: parseFloat(ethers3.formatUnits(alphaUSD, 6)),
2666
+ betaUSD: parseFloat(ethers3.formatUnits(betaUSD, 6)),
2667
+ thetaUSD: parseFloat(ethers3.formatUnits(thetaUSD, 6))
1251
2668
  }
1252
2669
  };
1253
2670
  } else {
1254
2671
  const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
1255
2672
  provider.getBalance(this.wallet.address),
1256
- new ethers2.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
1257
- new ethers2.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
2673
+ new ethers3.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
2674
+ new ethers3.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
1258
2675
  ]);
1259
2676
  results[chainName] = {
1260
- usdc: parseFloat(ethers2.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
1261
- usdt: parseFloat(ethers2.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
1262
- native: parseFloat(ethers2.formatEther(nativeBalance))
2677
+ usdc: parseFloat(ethers3.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
2678
+ usdt: parseFloat(ethers3.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
2679
+ native: parseFloat(ethers3.formatEther(nativeBalance))
1263
2680
  };
1264
2681
  }
1265
2682
  } catch (err) {
@@ -1386,6 +2803,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
1386
2803
  }
1387
2804
  };
1388
2805
  export {
1389
- MoltsPayClient
2806
+ MoltsPayClient,
2807
+ WechatClient
1390
2808
  };
1391
2809
  //# sourceMappingURL=index.mjs.map