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