moltspay 1.5.0 → 2.0.0

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