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.
- package/README.md +143 -0
- 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 +37 -1
- package/dist/chains/index.d.ts +37 -1
- package/dist/chains/index.js +22 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +19 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +1941 -204
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1938 -201
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +46 -0
- package/dist/client/index.d.ts +46 -0
- package/dist/client/index.js +1059 -118
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +1055 -116
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.d.mts +434 -0
- package/dist/client/web/index.mjs +1300 -0
- package/dist/client/web/index.mjs.map +1 -0
- package/dist/facilitators/index.d.mts +185 -6
- package/dist/facilitators/index.d.ts +185 -6
- package/dist/facilitators/index.js +497 -13
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +492 -13
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/index.d.mts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js +1865 -172
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1854 -167
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.js +1062 -123
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/index.mjs +1059 -120
- 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 +137 -2
- package/dist/server/index.d.ts +137 -2
- package/dist/server/index.js +757 -30
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +757 -30
- 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 +15 -2
- package/schemas/moltspay.services.schema.json +100 -6
- package/scripts/postinstall.js +91 -0
package/dist/cli/index.js
CHANGED
|
@@ -37,18 +37,21 @@ var init_cjs_shims = __esm({
|
|
|
37
37
|
init_cjs_shims();
|
|
38
38
|
var import_crypto = require("crypto");
|
|
39
39
|
var import_commander = require("commander");
|
|
40
|
-
var
|
|
41
|
-
var
|
|
40
|
+
var import_os4 = require("os");
|
|
41
|
+
var import_path4 = require("path");
|
|
42
42
|
var import_fs5 = require("fs");
|
|
43
|
-
var
|
|
44
|
-
var
|
|
43
|
+
var import_child_process3 = require("child_process");
|
|
44
|
+
var import_ethers4 = require("ethers");
|
|
45
45
|
|
|
46
46
|
// src/client/index.ts
|
|
47
47
|
init_cjs_shims();
|
|
48
|
+
|
|
49
|
+
// src/client/node/index.ts
|
|
50
|
+
init_cjs_shims();
|
|
48
51
|
var import_fs2 = require("fs");
|
|
49
|
-
var
|
|
50
|
-
var
|
|
51
|
-
var
|
|
52
|
+
var import_os3 = require("os");
|
|
53
|
+
var import_path3 = require("path");
|
|
54
|
+
var import_ethers2 = require("ethers");
|
|
52
55
|
|
|
53
56
|
// src/chains/index.ts
|
|
54
57
|
init_cjs_shims();
|
|
@@ -224,6 +227,10 @@ function getChain(name) {
|
|
|
224
227
|
}
|
|
225
228
|
return config;
|
|
226
229
|
}
|
|
230
|
+
var ALIPAY_CHAIN_ID = "alipay";
|
|
231
|
+
function isAlipayChainId(id) {
|
|
232
|
+
return id === ALIPAY_CHAIN_ID;
|
|
233
|
+
}
|
|
227
234
|
|
|
228
235
|
// src/wallet/solana.ts
|
|
229
236
|
init_cjs_shims();
|
|
@@ -532,16 +539,16 @@ var SolanaFacilitator = class extends BaseFacilitator {
|
|
|
532
539
|
return this.supportedNetworks.includes(network);
|
|
533
540
|
}
|
|
534
541
|
};
|
|
535
|
-
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
542
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey, connection) {
|
|
536
543
|
const chainConfig = SOLANA_CHAINS[chain];
|
|
537
|
-
const
|
|
544
|
+
const conn = connection ?? new import_web33.Connection(chainConfig.rpc, "confirmed");
|
|
538
545
|
const mint = new import_web33.PublicKey(chainConfig.tokens.USDC.mint);
|
|
539
546
|
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
540
547
|
const senderATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, senderPubkey);
|
|
541
548
|
const recipientATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, recipientPubkey);
|
|
542
549
|
const transaction = new import_web33.Transaction();
|
|
543
550
|
try {
|
|
544
|
-
await (0, import_spl_token2.getAccount)(
|
|
551
|
+
await (0, import_spl_token2.getAccount)(conn, recipientATA);
|
|
545
552
|
} catch {
|
|
546
553
|
transaction.add(
|
|
547
554
|
(0, import_spl_token2.createAssociatedTokenAccountInstruction)(
|
|
@@ -572,22 +579,944 @@ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amo
|
|
|
572
579
|
// decimals
|
|
573
580
|
)
|
|
574
581
|
);
|
|
575
|
-
const { blockhash, lastValidBlockHeight } = await
|
|
582
|
+
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash();
|
|
576
583
|
transaction.recentBlockhash = blockhash;
|
|
577
584
|
transaction.feePayer = actualFeePayer;
|
|
578
585
|
return transaction;
|
|
579
586
|
}
|
|
580
587
|
|
|
581
|
-
// src/client/index.ts
|
|
582
|
-
var
|
|
588
|
+
// src/client/node/index.ts
|
|
589
|
+
var import_web35 = require("@solana/web3.js");
|
|
583
590
|
|
|
584
|
-
// src/client/
|
|
591
|
+
// src/client/core/index.ts
|
|
585
592
|
init_cjs_shims();
|
|
586
593
|
|
|
587
|
-
// src/client/
|
|
594
|
+
// src/client/core/types.ts
|
|
595
|
+
init_cjs_shims();
|
|
588
596
|
var X402_VERSION = 2;
|
|
589
597
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
590
598
|
var PAYMENT_HEADER = "x-payment";
|
|
599
|
+
|
|
600
|
+
// src/client/core/chain-map.ts
|
|
601
|
+
init_cjs_shims();
|
|
602
|
+
var NETWORK_TO_CHAIN = {
|
|
603
|
+
"eip155:8453": "base",
|
|
604
|
+
"eip155:137": "polygon",
|
|
605
|
+
"eip155:84532": "base_sepolia",
|
|
606
|
+
"eip155:42431": "tempo_moderato",
|
|
607
|
+
"eip155:56": "bnb",
|
|
608
|
+
"eip155:97": "bnb_testnet",
|
|
609
|
+
"solana:mainnet": "solana",
|
|
610
|
+
"solana:devnet": "solana_devnet"
|
|
611
|
+
};
|
|
612
|
+
var CHAIN_TO_NETWORK = Object.fromEntries(
|
|
613
|
+
Object.entries(NETWORK_TO_CHAIN).map(([network, chain]) => [chain, network])
|
|
614
|
+
);
|
|
615
|
+
function networkToChainName(network) {
|
|
616
|
+
return NETWORK_TO_CHAIN[network] ?? null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/client/core/base64.ts
|
|
620
|
+
init_cjs_shims();
|
|
621
|
+
var BufferCtor = globalThis.Buffer;
|
|
622
|
+
|
|
623
|
+
// src/client/core/errors.ts
|
|
624
|
+
init_cjs_shims();
|
|
625
|
+
var MoltsPayError = class extends Error {
|
|
626
|
+
code;
|
|
627
|
+
constructor(code, message) {
|
|
628
|
+
super(message);
|
|
629
|
+
this.name = "MoltsPayError";
|
|
630
|
+
this.code = code;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// src/client/core/eip3009.ts
|
|
635
|
+
init_cjs_shims();
|
|
636
|
+
var EIP3009_TYPES = {
|
|
637
|
+
TransferWithAuthorization: [
|
|
638
|
+
{ name: "from", type: "address" },
|
|
639
|
+
{ name: "to", type: "address" },
|
|
640
|
+
{ name: "value", type: "uint256" },
|
|
641
|
+
{ name: "validAfter", type: "uint256" },
|
|
642
|
+
{ name: "validBefore", type: "uint256" },
|
|
643
|
+
{ name: "nonce", type: "bytes32" }
|
|
644
|
+
]
|
|
645
|
+
};
|
|
646
|
+
function buildEIP3009TypedData(args) {
|
|
647
|
+
const validAfter = args.validAfter ?? "0";
|
|
648
|
+
const validBefore = args.validBefore ?? (Math.floor(Date.now() / 1e3) + 3600).toString();
|
|
649
|
+
const authorization = {
|
|
650
|
+
from: args.from,
|
|
651
|
+
to: args.to,
|
|
652
|
+
value: args.value,
|
|
653
|
+
validAfter,
|
|
654
|
+
validBefore,
|
|
655
|
+
nonce: args.nonce
|
|
656
|
+
};
|
|
657
|
+
return {
|
|
658
|
+
domain: {
|
|
659
|
+
name: args.tokenName,
|
|
660
|
+
version: args.tokenVersion,
|
|
661
|
+
chainId: args.chainId,
|
|
662
|
+
verifyingContract: args.tokenAddress
|
|
663
|
+
},
|
|
664
|
+
types: EIP3009_TYPES,
|
|
665
|
+
primaryType: "TransferWithAuthorization",
|
|
666
|
+
message: authorization
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/client/core/eip2612.ts
|
|
671
|
+
init_cjs_shims();
|
|
672
|
+
|
|
673
|
+
// src/client/core/bnb-intent.ts
|
|
674
|
+
init_cjs_shims();
|
|
675
|
+
var BNB_INTENT_TYPES = {
|
|
676
|
+
PaymentIntent: [
|
|
677
|
+
{ name: "from", type: "address" },
|
|
678
|
+
{ name: "to", type: "address" },
|
|
679
|
+
{ name: "amount", type: "uint256" },
|
|
680
|
+
{ name: "token", type: "address" },
|
|
681
|
+
{ name: "service", type: "string" },
|
|
682
|
+
{ name: "nonce", type: "uint256" },
|
|
683
|
+
{ name: "deadline", type: "uint256" }
|
|
684
|
+
]
|
|
685
|
+
};
|
|
686
|
+
var BNB_DOMAIN_NAME = "MoltsPay";
|
|
687
|
+
var BNB_DOMAIN_VERSION = "1";
|
|
688
|
+
function buildBnbIntentTypedData(args) {
|
|
689
|
+
const intent = {
|
|
690
|
+
from: args.from,
|
|
691
|
+
to: args.to,
|
|
692
|
+
amount: args.amount,
|
|
693
|
+
token: args.tokenAddress,
|
|
694
|
+
service: args.service,
|
|
695
|
+
nonce: args.nonce,
|
|
696
|
+
deadline: args.deadline
|
|
697
|
+
};
|
|
698
|
+
return {
|
|
699
|
+
domain: {
|
|
700
|
+
name: BNB_DOMAIN_NAME,
|
|
701
|
+
version: BNB_DOMAIN_VERSION,
|
|
702
|
+
chainId: args.chainId
|
|
703
|
+
},
|
|
704
|
+
types: BNB_INTENT_TYPES,
|
|
705
|
+
primaryType: "PaymentIntent",
|
|
706
|
+
message: intent
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/client/core/solana-tx.ts
|
|
711
|
+
init_cjs_shims();
|
|
712
|
+
|
|
713
|
+
// src/client/core/x402.ts
|
|
714
|
+
init_cjs_shims();
|
|
715
|
+
|
|
716
|
+
// src/client/node/signer.ts
|
|
717
|
+
init_cjs_shims();
|
|
718
|
+
var import_ethers = require("ethers");
|
|
719
|
+
var import_web34 = require("@solana/web3.js");
|
|
720
|
+
var NodeSigner = class {
|
|
721
|
+
evmWallet;
|
|
722
|
+
getSolanaKeypair;
|
|
723
|
+
constructor(evmWallet, options = {}) {
|
|
724
|
+
this.evmWallet = evmWallet;
|
|
725
|
+
this.getSolanaKeypair = options.getSolanaKeypair ?? (() => null);
|
|
726
|
+
}
|
|
727
|
+
async getEvmAddress() {
|
|
728
|
+
return this.evmWallet.address;
|
|
729
|
+
}
|
|
730
|
+
async getSolanaAddress() {
|
|
731
|
+
const kp = this.getSolanaKeypair();
|
|
732
|
+
return kp ? kp.publicKey.toBase58() : null;
|
|
733
|
+
}
|
|
734
|
+
async signTypedData(envelope) {
|
|
735
|
+
const mutableTypes = {};
|
|
736
|
+
for (const [key, fields] of Object.entries(envelope.types)) {
|
|
737
|
+
mutableTypes[key] = [...fields];
|
|
738
|
+
}
|
|
739
|
+
return this.evmWallet.signTypedData(
|
|
740
|
+
envelope.domain,
|
|
741
|
+
mutableTypes,
|
|
742
|
+
envelope.message
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
async sendEvmTransaction(args) {
|
|
746
|
+
const chain = findChainByChainId(args.chainId);
|
|
747
|
+
if (!chain) {
|
|
748
|
+
throw new Error(`sendEvmTransaction: unknown chainId ${args.chainId}`);
|
|
749
|
+
}
|
|
750
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
751
|
+
const connected = this.evmWallet.connect(provider);
|
|
752
|
+
const tx = await connected.sendTransaction({
|
|
753
|
+
to: args.to,
|
|
754
|
+
data: args.data,
|
|
755
|
+
value: args.value ? BigInt(args.value) : 0n
|
|
756
|
+
});
|
|
757
|
+
return tx.hash;
|
|
758
|
+
}
|
|
759
|
+
async signSolanaTransaction(args) {
|
|
760
|
+
const kp = this.getSolanaKeypair();
|
|
761
|
+
if (!kp) {
|
|
762
|
+
throw new Error("signSolanaTransaction: no Solana wallet configured");
|
|
763
|
+
}
|
|
764
|
+
const tx = import_web34.Transaction.from(Buffer.from(args.transactionBase64, "base64"));
|
|
765
|
+
if (args.partialSign) {
|
|
766
|
+
tx.partialSign(kp);
|
|
767
|
+
} else {
|
|
768
|
+
tx.sign(kp);
|
|
769
|
+
}
|
|
770
|
+
return tx.serialize({ requireAllSignatures: false }).toString("base64");
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
function findChainByChainId(chainId) {
|
|
774
|
+
for (const cfg of Object.values(CHAINS)) {
|
|
775
|
+
if (cfg.chainId === chainId) {
|
|
776
|
+
return cfg;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return void 0;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/client/alipay/index.ts
|
|
783
|
+
init_cjs_shims();
|
|
784
|
+
var import_node_crypto = require("crypto");
|
|
785
|
+
var import_promises = require("fs/promises");
|
|
786
|
+
var import_os2 = require("os");
|
|
787
|
+
var import_path2 = require("path");
|
|
788
|
+
|
|
789
|
+
// src/client/alipay/cli.ts
|
|
790
|
+
init_cjs_shims();
|
|
791
|
+
var import_child_process = require("child_process");
|
|
792
|
+
|
|
793
|
+
// src/client/alipay/log.ts
|
|
794
|
+
init_cjs_shims();
|
|
795
|
+
var RANK = { off: 0, info: 1, debug: 2 };
|
|
796
|
+
function normalizeLevel(v) {
|
|
797
|
+
const s = (v ?? "").toLowerCase();
|
|
798
|
+
return s === "info" || s === "debug" ? s : "off";
|
|
799
|
+
}
|
|
800
|
+
var level = normalizeLevel(process.env.MOLTSPAY_ALIPAY_LOG);
|
|
801
|
+
var sink = (line) => {
|
|
802
|
+
process.stderr.write(line + "\n");
|
|
803
|
+
};
|
|
804
|
+
function enabledFor(want) {
|
|
805
|
+
return RANK[level] >= RANK[want];
|
|
806
|
+
}
|
|
807
|
+
function render(event, fields) {
|
|
808
|
+
const parts = ["[moltspay:alipay]", String(fields.ts), event];
|
|
809
|
+
if (fields.flow) parts.push(`flow=${fields.flow}`);
|
|
810
|
+
if (fields.step) parts.push(`step=${fields.step}`);
|
|
811
|
+
if (typeof fields.ms === "number") parts.push(`${fields.ms}ms`);
|
|
812
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
813
|
+
if (["ts", "level", "event", "flow", "step", "ms"].includes(k)) continue;
|
|
814
|
+
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
815
|
+
}
|
|
816
|
+
return parts.join(" ");
|
|
817
|
+
}
|
|
818
|
+
function emit(lvl, event, fields) {
|
|
819
|
+
if (!enabledFor(lvl)) return;
|
|
820
|
+
const full = { ts: (/* @__PURE__ */ new Date()).toISOString(), level: lvl, event, ...fields };
|
|
821
|
+
try {
|
|
822
|
+
sink(render(event, full), full);
|
|
823
|
+
} catch {
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
var alipayLog = {
|
|
827
|
+
info: (event, fields = {}) => emit("info", event, fields),
|
|
828
|
+
debug: (event, fields = {}) => emit("debug", event, fields),
|
|
829
|
+
/** True if anything at all would be logged (cheap guard for hot paths). */
|
|
830
|
+
enabled: () => level !== "off"
|
|
831
|
+
};
|
|
832
|
+
async function timeStep(step, flow, fn) {
|
|
833
|
+
if (!alipayLog.enabled()) return fn();
|
|
834
|
+
const t0 = Date.now();
|
|
835
|
+
alipayLog.debug("step.start", { flow, step });
|
|
836
|
+
try {
|
|
837
|
+
const out = await fn();
|
|
838
|
+
alipayLog.info("step.end", { flow, step, ms: Date.now() - t0, ok: true });
|
|
839
|
+
return out;
|
|
840
|
+
} catch (err) {
|
|
841
|
+
alipayLog.info("step.end", {
|
|
842
|
+
flow,
|
|
843
|
+
step,
|
|
844
|
+
ms: Date.now() - t0,
|
|
845
|
+
ok: false,
|
|
846
|
+
err: err instanceof Error ? err.message : String(err)
|
|
847
|
+
});
|
|
848
|
+
throw err;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/client/alipay/cli.ts
|
|
853
|
+
var ALLOWED_ENV = /* @__PURE__ */ new Set([
|
|
854
|
+
"AIPAY_OUTPUT_CHANNEL",
|
|
855
|
+
"AIPAY_SESSION_ID",
|
|
856
|
+
"AIPAY_FRAMEWORK",
|
|
857
|
+
"AIPAY_MODEL",
|
|
858
|
+
"AIPAY_OS",
|
|
859
|
+
// Minimal survival set for spawn to find the binary and a home dir.
|
|
860
|
+
"PATH",
|
|
861
|
+
"HOME"
|
|
862
|
+
]);
|
|
863
|
+
function filterEnv(env) {
|
|
864
|
+
return Object.fromEntries(
|
|
865
|
+
Object.entries(env).filter(([k]) => ALLOWED_ENV.has(k))
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
function makeLineSplitter(onLine) {
|
|
869
|
+
let buf = "";
|
|
870
|
+
return (chunk) => {
|
|
871
|
+
buf += chunk.toString("utf-8");
|
|
872
|
+
let nl;
|
|
873
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
874
|
+
onLine(buf.slice(0, nl));
|
|
875
|
+
buf = buf.slice(nl + 1);
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function profileEnv(step, flow) {
|
|
880
|
+
const want = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE;
|
|
881
|
+
const hook = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_HOOK;
|
|
882
|
+
if (!want || !hook) return {};
|
|
883
|
+
const steps = want.split(",").map((s) => s.trim());
|
|
884
|
+
if (!steps.includes("all") && !steps.includes(step)) return {};
|
|
885
|
+
const dir = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_DIR || "/tmp";
|
|
886
|
+
const safe = `${flow ?? "noflow"}-${step}-${Date.now()}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
887
|
+
const out = `${dir}/cli-profile-${safe}.json`;
|
|
888
|
+
alipayLog.info("cli.profile", { flow, step, out });
|
|
889
|
+
return {
|
|
890
|
+
NODE_OPTIONS: `--require=${hook}`,
|
|
891
|
+
MOLTSPAY_CLI_PROFILE_OUT: out
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
var runCli = (args, opts = {}) => {
|
|
895
|
+
const bin = opts.bin ?? "alipay-bot";
|
|
896
|
+
const step = opts.step ?? args[0] ?? "(no-arg)";
|
|
897
|
+
const startedAt = Date.now();
|
|
898
|
+
const lines = [];
|
|
899
|
+
const collect = (line) => {
|
|
900
|
+
lines.push(line);
|
|
901
|
+
alipayLog.debug("cli.line", {
|
|
902
|
+
flow: opts.flow,
|
|
903
|
+
step,
|
|
904
|
+
ms: Date.now() - startedAt,
|
|
905
|
+
preview: line.slice(0, 120)
|
|
906
|
+
});
|
|
907
|
+
opts.onLine?.(line);
|
|
908
|
+
};
|
|
909
|
+
alipayLog.debug("step.start", { flow: opts.flow, step });
|
|
910
|
+
return new Promise((resolve3, reject) => {
|
|
911
|
+
const child = (0, import_child_process.spawn)(bin, args, {
|
|
912
|
+
env: { ...filterEnv(process.env), ...profileEnv(step, opts.flow), ...opts.env ?? {} }
|
|
913
|
+
});
|
|
914
|
+
if (opts.signal) {
|
|
915
|
+
if (opts.signal.aborted) child.kill("SIGTERM");
|
|
916
|
+
opts.signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
|
|
917
|
+
}
|
|
918
|
+
let sawByte = false;
|
|
919
|
+
const onChunk = (stream) => (chunk) => {
|
|
920
|
+
const ms = Date.now() - startedAt;
|
|
921
|
+
if (!sawByte) {
|
|
922
|
+
sawByte = true;
|
|
923
|
+
alipayLog.debug("cli.firstbyte", { flow: opts.flow, step, ms });
|
|
924
|
+
}
|
|
925
|
+
alipayLog.debug("cli.chunk", { flow: opts.flow, step, ms, stream, bytes: chunk.length });
|
|
926
|
+
};
|
|
927
|
+
const onStdout = makeLineSplitter(collect);
|
|
928
|
+
const onStderr = makeLineSplitter(collect);
|
|
929
|
+
child.stdout?.on("data", onChunk("out"));
|
|
930
|
+
child.stderr?.on("data", onChunk("err"));
|
|
931
|
+
child.stdout?.on("data", onStdout);
|
|
932
|
+
child.stderr?.on("data", onStderr);
|
|
933
|
+
child.on("error", (err) => {
|
|
934
|
+
alipayLog.info("cli.exit", {
|
|
935
|
+
flow: opts.flow,
|
|
936
|
+
step,
|
|
937
|
+
ms: Date.now() - startedAt,
|
|
938
|
+
ok: false,
|
|
939
|
+
err: err.message
|
|
940
|
+
});
|
|
941
|
+
reject(err);
|
|
942
|
+
});
|
|
943
|
+
child.on("close", (code) => {
|
|
944
|
+
alipayLog.info("cli.exit", {
|
|
945
|
+
flow: opts.flow,
|
|
946
|
+
step,
|
|
947
|
+
ms: Date.now() - startedAt,
|
|
948
|
+
ok: (code ?? 1) === 0,
|
|
949
|
+
code: code ?? 1
|
|
950
|
+
});
|
|
951
|
+
resolve3({ exitCode: code ?? 1, lines });
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
// src/client/alipay/install.ts
|
|
957
|
+
init_cjs_shims();
|
|
958
|
+
var import_child_process2 = require("child_process");
|
|
959
|
+
var import_util = require("util");
|
|
960
|
+
|
|
961
|
+
// src/client/alipay/errors.ts
|
|
962
|
+
init_cjs_shims();
|
|
963
|
+
var AlipayCliNotFoundError = class extends MoltsPayError {
|
|
964
|
+
constructor(message) {
|
|
965
|
+
super("ALIPAY_CLI_NOT_FOUND", message);
|
|
966
|
+
this.name = "AlipayCliNotFoundError";
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
var AlipayCliVersionError = class extends MoltsPayError {
|
|
970
|
+
constructor(message) {
|
|
971
|
+
super("ALIPAY_CLI_VERSION", message);
|
|
972
|
+
this.name = "AlipayCliVersionError";
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
var NeedsWalletSetupError = class extends MoltsPayError {
|
|
976
|
+
constructor(message = "Alipay wallet not set up. Run: moltspay alipay apply") {
|
|
977
|
+
super("ALIPAY_NEEDS_WALLET_SETUP", message);
|
|
978
|
+
this.name = "NeedsWalletSetupError";
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
var AlipayPaymentRejectedError = class extends MoltsPayError {
|
|
982
|
+
constructor(message) {
|
|
983
|
+
super("ALIPAY_PAYMENT_REJECTED", message);
|
|
984
|
+
this.name = "AlipayPaymentRejectedError";
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
var AlipayPaymentTimeoutError = class extends MoltsPayError {
|
|
988
|
+
constructor(message) {
|
|
989
|
+
super("ALIPAY_PAYMENT_TIMEOUT", message);
|
|
990
|
+
this.name = "AlipayPaymentTimeoutError";
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
var AlipayProtocolError = class extends MoltsPayError {
|
|
994
|
+
constructor(message) {
|
|
995
|
+
super("ALIPAY_PROTOCOL", message);
|
|
996
|
+
this.name = "AlipayProtocolError";
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
var UnsupportedRailError = class extends MoltsPayError {
|
|
1000
|
+
constructor(rail, message) {
|
|
1001
|
+
super("UNSUPPORTED_RAIL", message ?? `Rail not supported by server: ${rail}`);
|
|
1002
|
+
this.rail = rail;
|
|
1003
|
+
this.name = "UnsupportedRailError";
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// src/client/alipay/install.ts
|
|
1008
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
|
|
1009
|
+
var MIN_CLI_VERSION = "0.3.15";
|
|
1010
|
+
function semverLt(a, b) {
|
|
1011
|
+
const pa = a.split(".").map((n) => parseInt(n, 10));
|
|
1012
|
+
const pb = b.split(".").map((n) => parseInt(n, 10));
|
|
1013
|
+
for (let i = 0; i < 3; i++) {
|
|
1014
|
+
const x = pa[i] ?? 0;
|
|
1015
|
+
const y = pb[i] ?? 0;
|
|
1016
|
+
if (x !== y) return x < y;
|
|
1017
|
+
}
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
function parseVersion(stdout) {
|
|
1021
|
+
return stdout.match(/v?(\d+\.\d+\.\d+)/)?.[1] ?? null;
|
|
1022
|
+
}
|
|
1023
|
+
var defaultGetVersion = async () => {
|
|
1024
|
+
const { stdout } = await execFileAsync("alipay-bot", ["--version"]);
|
|
1025
|
+
return stdout;
|
|
1026
|
+
};
|
|
1027
|
+
async function ensureCliUncached(getVersion2) {
|
|
1028
|
+
let stdout;
|
|
1029
|
+
try {
|
|
1030
|
+
stdout = await getVersion2();
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
if (e?.code === "ENOENT") {
|
|
1033
|
+
throw new AlipayCliNotFoundError(
|
|
1034
|
+
"alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
throw e;
|
|
1038
|
+
}
|
|
1039
|
+
const version = parseVersion(stdout);
|
|
1040
|
+
if (!version || semverLt(version, MIN_CLI_VERSION)) {
|
|
1041
|
+
throw new AlipayCliVersionError(
|
|
1042
|
+
`alipay-bot ${version ?? "?"} found, need \u2265 ${MIN_CLI_VERSION}. Run: npx -y @alipay/agent-payment@latest update`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
return version;
|
|
1046
|
+
}
|
|
1047
|
+
var cachedVersion = null;
|
|
1048
|
+
var inflight = null;
|
|
1049
|
+
async function ensureCli(getVersion2 = defaultGetVersion) {
|
|
1050
|
+
if (getVersion2 !== defaultGetVersion) {
|
|
1051
|
+
return ensureCliUncached(getVersion2);
|
|
1052
|
+
}
|
|
1053
|
+
if (cachedVersion) return cachedVersion;
|
|
1054
|
+
if (!inflight) {
|
|
1055
|
+
inflight = ensureCliUncached(getVersion2).then((v) => {
|
|
1056
|
+
cachedVersion = v;
|
|
1057
|
+
return v;
|
|
1058
|
+
}).finally(() => {
|
|
1059
|
+
inflight = null;
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
return inflight;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/client/alipay/poll.ts
|
|
1066
|
+
init_cjs_shims();
|
|
1067
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
1068
|
+
var POLL_MAX_INFLIGHT = 2;
|
|
1069
|
+
function resolveMaxInflight(override) {
|
|
1070
|
+
if (override !== void 0) return Math.max(1, Math.floor(override));
|
|
1071
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_MAX_INFLIGHT;
|
|
1072
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
1073
|
+
const n = Number(raw);
|
|
1074
|
+
if (Number.isFinite(n) && n >= 1) return Math.floor(n);
|
|
1075
|
+
}
|
|
1076
|
+
return POLL_MAX_INFLIGHT;
|
|
1077
|
+
}
|
|
1078
|
+
function resolveLaunchGap(override) {
|
|
1079
|
+
if (override !== void 0) return Math.max(0, override);
|
|
1080
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_LAUNCH_MS;
|
|
1081
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
1082
|
+
const n = Number(raw);
|
|
1083
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
1084
|
+
}
|
|
1085
|
+
return POLL_INTERVAL_MS;
|
|
1086
|
+
}
|
|
1087
|
+
function parseStatus(lines) {
|
|
1088
|
+
const raw = lines.join("\n").trim();
|
|
1089
|
+
try {
|
|
1090
|
+
const json = JSON.parse(raw);
|
|
1091
|
+
if (json && typeof json === "object") {
|
|
1092
|
+
if (typeof json.success === "boolean") {
|
|
1093
|
+
if (json.success) return "paid";
|
|
1094
|
+
const err = String(json.errorCode ?? "").toUpperCase();
|
|
1095
|
+
if (/UNPAID|WAIT|PENDING|PROCESS|NOTPAY/.test(err)) return "pending";
|
|
1096
|
+
if (/CLOSED|CANCEL|FAIL|REJECT|REFUSE|TIMEOUT|EXPIRE/.test(err)) return "rejected";
|
|
1097
|
+
return "pending";
|
|
1098
|
+
}
|
|
1099
|
+
if (typeof json.code !== "undefined") {
|
|
1100
|
+
if (Number(json.code) === 200) return "paid";
|
|
1101
|
+
const msg = `${json.message ?? ""}${json.reason ?? ""}`;
|
|
1102
|
+
if (/关闭|失败|拒绝|取消|超时|已撤销/.test(msg)) return "rejected";
|
|
1103
|
+
return "pending";
|
|
1104
|
+
}
|
|
1105
|
+
if (typeof json.body === "string") {
|
|
1106
|
+
return classifyReportText(json.body);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
return classifyReportText(raw);
|
|
1112
|
+
}
|
|
1113
|
+
function classifyReportText(s) {
|
|
1114
|
+
const text = s.toUpperCase();
|
|
1115
|
+
if (/查询支付状态成功并获取资源/.test(s) || /资源响应状态[^0-9\n]{0,8}200/.test(s) || /TRADE_SUCCESS|TRADE_FINISHED|"STATUS":\s*"FULFILLED"/.test(text)) return "paid";
|
|
1116
|
+
if (/TRADE_CLOSED|REJECTED|REFUSE|CANCEL/.test(text) || /交易关闭|支付失败|已拒绝|已取消|已撤销|交易超时/.test(s)) return "rejected";
|
|
1117
|
+
if (/WAIT_BUYER_PAY|UNPAID|PENDING|WAITING/.test(text) || /交易未支付|等待付款|待支付|未支付/.test(s)) return "pending";
|
|
1118
|
+
return "unknown";
|
|
1119
|
+
}
|
|
1120
|
+
function defaultSleep(ms, signal) {
|
|
1121
|
+
return new Promise((resolve3, reject) => {
|
|
1122
|
+
if (signal?.aborted) return reject(new Error("aborted"));
|
|
1123
|
+
const t = setTimeout(resolve3, ms);
|
|
1124
|
+
signal?.addEventListener(
|
|
1125
|
+
"abort",
|
|
1126
|
+
() => {
|
|
1127
|
+
clearTimeout(t);
|
|
1128
|
+
reject(new Error("aborted"));
|
|
1129
|
+
},
|
|
1130
|
+
{ once: true }
|
|
1131
|
+
);
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
var LAUNCH = /* @__PURE__ */ Symbol("launch");
|
|
1135
|
+
async function pollUntil(tradeNo, resourceUrl, opts) {
|
|
1136
|
+
const runner = opts.runner ?? runCli;
|
|
1137
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
1138
|
+
const now = opts.now ?? Date.now;
|
|
1139
|
+
const maxInflight = resolveMaxInflight(opts.maxInflight);
|
|
1140
|
+
const launchGap = resolveLaunchGap(opts.launchIntervalMs);
|
|
1141
|
+
const args = [
|
|
1142
|
+
"402-query-payment-status",
|
|
1143
|
+
"-t",
|
|
1144
|
+
tradeNo,
|
|
1145
|
+
"-r",
|
|
1146
|
+
resourceUrl,
|
|
1147
|
+
...opts.method ? ["-m", opts.method] : [],
|
|
1148
|
+
...opts.data ? ["-d", opts.data] : []
|
|
1149
|
+
];
|
|
1150
|
+
const internal = new AbortController();
|
|
1151
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, internal.signal]) : internal.signal;
|
|
1152
|
+
let tick = 0;
|
|
1153
|
+
let lastLaunch = -Infinity;
|
|
1154
|
+
const inflight2 = /* @__PURE__ */ new Set();
|
|
1155
|
+
const launch = () => {
|
|
1156
|
+
tick += 1;
|
|
1157
|
+
const myTick = tick;
|
|
1158
|
+
lastLaunch = now();
|
|
1159
|
+
const run = (async () => {
|
|
1160
|
+
const { lines } = await runner(args, {
|
|
1161
|
+
signal,
|
|
1162
|
+
step: "query-payment-status",
|
|
1163
|
+
flow: tradeNo
|
|
1164
|
+
});
|
|
1165
|
+
const status = parseStatus(lines);
|
|
1166
|
+
alipayLog.debug("poll.tick", { flow: tradeNo, tick: myTick, status });
|
|
1167
|
+
return { status, lines };
|
|
1168
|
+
})();
|
|
1169
|
+
const tracked = run.finally(() => {
|
|
1170
|
+
inflight2.delete(tracked);
|
|
1171
|
+
});
|
|
1172
|
+
inflight2.add(tracked);
|
|
1173
|
+
};
|
|
1174
|
+
try {
|
|
1175
|
+
for (; ; ) {
|
|
1176
|
+
if (opts.signal?.aborted) throw new Error("aborted");
|
|
1177
|
+
const expired = now() >= opts.deadline;
|
|
1178
|
+
if (expired && inflight2.size === 0) {
|
|
1179
|
+
throw new AlipayPaymentTimeoutError(
|
|
1180
|
+
`Payment ${tradeNo} not completed before pay_before deadline`
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
while (!expired && inflight2.size < maxInflight && now() - lastLaunch >= launchGap) {
|
|
1184
|
+
launch();
|
|
1185
|
+
}
|
|
1186
|
+
const waiters = [...inflight2];
|
|
1187
|
+
if (!expired && inflight2.size < maxInflight) {
|
|
1188
|
+
const untilNext = Math.max(0, launchGap - (now() - lastLaunch));
|
|
1189
|
+
waiters.push(sleep(untilNext, signal).then(() => LAUNCH, () => LAUNCH));
|
|
1190
|
+
}
|
|
1191
|
+
const winner = await Promise.race(waiters);
|
|
1192
|
+
if (winner === LAUNCH) continue;
|
|
1193
|
+
if (winner.status === "paid") return { status: "paid", lines: winner.lines };
|
|
1194
|
+
if (winner.status === "rejected") {
|
|
1195
|
+
throw new AlipayPaymentRejectedError(`Payment ${tradeNo} was rejected`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
} finally {
|
|
1199
|
+
internal.abort();
|
|
1200
|
+
for (const p of inflight2) p.catch(() => void 0);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/client/alipay/index.ts
|
|
1205
|
+
var TRADE_NO_RE = /^\d{32}$/;
|
|
1206
|
+
function resolveSessionId(explicit, envSession) {
|
|
1207
|
+
return explicit ?? envSession ?? (0, import_node_crypto.randomUUID)();
|
|
1208
|
+
}
|
|
1209
|
+
function assertTradeNo(t) {
|
|
1210
|
+
if (!TRADE_NO_RE.test(t)) {
|
|
1211
|
+
throw new AlipayProtocolError(`invalid tradeNo (expect 32 digits): ${t}`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
function parseTradeNo(lines) {
|
|
1215
|
+
for (const line of lines) {
|
|
1216
|
+
const labeled = line.match(/trade[_-]?no["'\s:=]+(\d{32})/i);
|
|
1217
|
+
if (labeled) return labeled[1];
|
|
1218
|
+
const bare = line.match(/\b(\d{32})\b/);
|
|
1219
|
+
if (bare) return bare[1];
|
|
1220
|
+
}
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
function parsePaymentUrl(lines) {
|
|
1224
|
+
let paymentUrl;
|
|
1225
|
+
let shortenUrl;
|
|
1226
|
+
for (const line of lines) {
|
|
1227
|
+
const m = line.match(/(alipays?:\/\/\S+|https?:\/\/\S+)/i);
|
|
1228
|
+
if (!m) continue;
|
|
1229
|
+
const url = m[1].replace(/[)\]`>]+$/, "");
|
|
1230
|
+
if (/short|qr\.alipay|surl|\/s\//i.test(line) && !shortenUrl) shortenUrl = url;
|
|
1231
|
+
else if (!paymentUrl) paymentUrl = url;
|
|
1232
|
+
}
|
|
1233
|
+
if (!paymentUrl && shortenUrl) paymentUrl = shortenUrl;
|
|
1234
|
+
return { paymentUrl, shortenUrl };
|
|
1235
|
+
}
|
|
1236
|
+
function extractMedia(line) {
|
|
1237
|
+
const m = line.match(/^\s*MEDIA:\s*(.+?)\s*$/);
|
|
1238
|
+
return m ? m[1] : null;
|
|
1239
|
+
}
|
|
1240
|
+
function isWalletReady(lines) {
|
|
1241
|
+
const text = lines.join("\n").trim();
|
|
1242
|
+
try {
|
|
1243
|
+
const json = JSON.parse(text);
|
|
1244
|
+
if (json && typeof json.code !== "undefined") return Number(json.code) === 200;
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
return !/未开通|未开启|NOT[_\s-]*(OPEN|BOUND|SET)|NEEDS?[_\s-]*SETUP|NO[_\s-]*WALLET/i.test(text);
|
|
1248
|
+
}
|
|
1249
|
+
function extractResourceFromReport(report) {
|
|
1250
|
+
const label = report.search(/资源响应体/);
|
|
1251
|
+
const start = report.indexOf("{", label === -1 ? 0 : label);
|
|
1252
|
+
if (start === -1) return void 0;
|
|
1253
|
+
let depth = 0, inStr = false, esc = false;
|
|
1254
|
+
for (let i = start; i < report.length; i++) {
|
|
1255
|
+
const ch = report[i];
|
|
1256
|
+
if (inStr) {
|
|
1257
|
+
if (esc) esc = false;
|
|
1258
|
+
else if (ch === "\\") esc = true;
|
|
1259
|
+
else if (ch === '"') inStr = false;
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
if (ch === '"') inStr = true;
|
|
1263
|
+
else if (ch === "{") depth++;
|
|
1264
|
+
else if (ch === "}" && --depth === 0) {
|
|
1265
|
+
const slice = report.slice(start, i + 1);
|
|
1266
|
+
try {
|
|
1267
|
+
const obj = JSON.parse(slice);
|
|
1268
|
+
const r = obj?.resourceResponse ?? obj?.result ?? obj?.data ?? obj?.body ?? obj;
|
|
1269
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
1270
|
+
} catch {
|
|
1271
|
+
return slice;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return void 0;
|
|
1276
|
+
}
|
|
1277
|
+
function extractBody(lines) {
|
|
1278
|
+
const text = lines.join("\n").trim();
|
|
1279
|
+
try {
|
|
1280
|
+
const json = JSON.parse(text);
|
|
1281
|
+
if (json && typeof json === "object") {
|
|
1282
|
+
if (json.resourceResponse !== void 0 && json.resourceResponse !== null) {
|
|
1283
|
+
const rr = json.resourceResponse;
|
|
1284
|
+
const r = typeof rr === "object" ? rr.result ?? rr.data ?? rr.body ?? rr : rr;
|
|
1285
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
1286
|
+
}
|
|
1287
|
+
if (typeof json.body === "string") {
|
|
1288
|
+
const inner = extractResourceFromReport(json.body);
|
|
1289
|
+
return inner ?? json.body;
|
|
1290
|
+
}
|
|
1291
|
+
const body = json.data ?? json.result ?? json.body ?? json.resource;
|
|
1292
|
+
if (body !== void 0) return typeof body === "string" ? body : JSON.stringify(body);
|
|
1293
|
+
return text;
|
|
1294
|
+
}
|
|
1295
|
+
} catch {
|
|
1296
|
+
}
|
|
1297
|
+
const idx = lines.findIndex((l) => /^\s*BODY:/.test(l));
|
|
1298
|
+
if (idx !== -1) {
|
|
1299
|
+
const first = lines[idx].replace(/^\s*BODY:\s*/, "");
|
|
1300
|
+
return [first, ...lines.slice(idx + 1)].join("\n").trim();
|
|
1301
|
+
}
|
|
1302
|
+
return lines.filter((l) => !/^\s*(MEDIA|STATUS|INFO):/.test(l)).join("\n").trim();
|
|
1303
|
+
}
|
|
1304
|
+
var DEFAULT_WALLET_TTL_MS = 10 * 60 * 1e3;
|
|
1305
|
+
function resolveWalletTtlMs() {
|
|
1306
|
+
const raw = process.env.MOLTSPAY_ALIPAY_WALLET_TTL_MS;
|
|
1307
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_WALLET_TTL_MS;
|
|
1308
|
+
const n = Number(raw);
|
|
1309
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_WALLET_TTL_MS;
|
|
1310
|
+
}
|
|
1311
|
+
var walletReadyUntil = /* @__PURE__ */ new Map();
|
|
1312
|
+
var DEFAULT_INTENT_TTL_MS = 10 * 60 * 1e3;
|
|
1313
|
+
function resolveIntentTtlMs() {
|
|
1314
|
+
const raw = process.env.MOLTSPAY_ALIPAY_INTENT_TTL_MS;
|
|
1315
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_INTENT_TTL_MS;
|
|
1316
|
+
const n = Number(raw);
|
|
1317
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTENT_TTL_MS;
|
|
1318
|
+
}
|
|
1319
|
+
var intentDoneUntil = /* @__PURE__ */ new Map();
|
|
1320
|
+
var AlipayClient = class {
|
|
1321
|
+
sessionId;
|
|
1322
|
+
configDir;
|
|
1323
|
+
framework;
|
|
1324
|
+
runner;
|
|
1325
|
+
getVersion;
|
|
1326
|
+
now;
|
|
1327
|
+
/** Only the default runner may use the process-level wallet cache. */
|
|
1328
|
+
walletCacheable;
|
|
1329
|
+
/** Only the default runner may use the process-level payment-intent cache. */
|
|
1330
|
+
intentCacheable;
|
|
1331
|
+
constructor(opts = {}) {
|
|
1332
|
+
this.sessionId = resolveSessionId(opts.sessionId, process.env.AIPAY_SESSION_ID);
|
|
1333
|
+
this.configDir = opts.configDir ?? (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
|
|
1334
|
+
this.framework = opts.framework ?? process.env.AIPAY_FRAMEWORK ?? "openclaw";
|
|
1335
|
+
this.runner = opts.runner ?? runCli;
|
|
1336
|
+
this.getVersion = opts.getVersion;
|
|
1337
|
+
this.now = opts.now ?? Date.now;
|
|
1338
|
+
this.walletCacheable = opts.cacheWallet ?? !opts.runner;
|
|
1339
|
+
this.intentCacheable = opts.cacheIntent ?? !opts.runner;
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Throws NeedsWalletSetupError unless alipay-bot reports an opened wallet.
|
|
1343
|
+
*
|
|
1344
|
+
* Skips the ~22s `check-wallet` spawn entirely when a prior call cached a
|
|
1345
|
+
* "ready" verdict within the TTL (see {@link DEFAULT_WALLET_TTL_MS}).
|
|
1346
|
+
*/
|
|
1347
|
+
async checkWallet(signal, flow) {
|
|
1348
|
+
const ttlMs = resolveWalletTtlMs();
|
|
1349
|
+
const cacheKey = `${this.configDir}::${this.framework}`;
|
|
1350
|
+
const useCache = this.walletCacheable && ttlMs > 0;
|
|
1351
|
+
if (useCache) {
|
|
1352
|
+
const until = walletReadyUntil.get(cacheKey);
|
|
1353
|
+
if (until !== void 0 && this.now() < until) {
|
|
1354
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: true, ttlMsLeft: until - this.now() });
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const { lines } = await this.runner(["check-wallet"], { signal, step: "check-wallet", flow });
|
|
1359
|
+
if (!isWalletReady(lines)) {
|
|
1360
|
+
if (this.walletCacheable) walletReadyUntil.delete(cacheKey);
|
|
1361
|
+
throw new NeedsWalletSetupError(
|
|
1362
|
+
"Alipay wallet not opened. Run: moltspay alipay apply (then: moltspay alipay bind)"
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
if (useCache) {
|
|
1366
|
+
walletReadyUntil.set(cacheKey, this.now() + ttlMs);
|
|
1367
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: false, cachedForMs: ttlMs });
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/** Run the full 8-step flow and resolve with the resource body. */
|
|
1371
|
+
async pay402(opts) {
|
|
1372
|
+
const { resourceUrl, requirement, signal } = opts;
|
|
1373
|
+
const extra = requirement.extra ?? {};
|
|
1374
|
+
const paymentNeededHeader = String(extra.payment_needed_header ?? "");
|
|
1375
|
+
if (!paymentNeededHeader) {
|
|
1376
|
+
throw new AlipayProtocolError("alipay requirement missing extra.payment_needed_header");
|
|
1377
|
+
}
|
|
1378
|
+
const flow = this.sessionId;
|
|
1379
|
+
const flowStart = this.now();
|
|
1380
|
+
alipayLog.info("flow.start", { flow, resource: resourceUrl });
|
|
1381
|
+
const media = [];
|
|
1382
|
+
const onLine = (line) => {
|
|
1383
|
+
const m = extractMedia(line);
|
|
1384
|
+
if (m) media.push(m);
|
|
1385
|
+
else opts.onLine?.(line);
|
|
1386
|
+
};
|
|
1387
|
+
const intentSummary = opts.intentSummary?.trim() || `\u652F\u4ED8 ${requirement.amount ?? ""} ${requirement.asset ?? "CNY"}`.trim();
|
|
1388
|
+
await timeStep("ensure-cli", flow, () => ensureCli(this.getVersion));
|
|
1389
|
+
const intentTtlMs = resolveIntentTtlMs();
|
|
1390
|
+
const intentKey = `${this.configDir}::${this.framework}`;
|
|
1391
|
+
const useIntentCache = this.intentCacheable && intentTtlMs > 0;
|
|
1392
|
+
const intentUntil = useIntentCache ? intentDoneUntil.get(intentKey) : void 0;
|
|
1393
|
+
if (intentUntil !== void 0 && this.now() < intentUntil) {
|
|
1394
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: true, ttlMsLeft: intentUntil - this.now() });
|
|
1395
|
+
} else {
|
|
1396
|
+
await this.runner(
|
|
1397
|
+
[
|
|
1398
|
+
"payment-intent",
|
|
1399
|
+
"--session-id",
|
|
1400
|
+
this.sessionId,
|
|
1401
|
+
"--intent-summary",
|
|
1402
|
+
intentSummary,
|
|
1403
|
+
"--framework",
|
|
1404
|
+
this.framework
|
|
1405
|
+
],
|
|
1406
|
+
{ onLine, signal, step: "payment-intent", flow }
|
|
1407
|
+
);
|
|
1408
|
+
if (useIntentCache) {
|
|
1409
|
+
intentDoneUntil.set(intentKey, this.now() + intentTtlMs);
|
|
1410
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: false, cachedForMs: intentTtlMs });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
await this.checkWallet(signal, flow);
|
|
1414
|
+
const reqId = String(extra.out_trade_no ?? (0, import_node_crypto.randomUUID)());
|
|
1415
|
+
const dir = (0, import_path2.join)(this.configDir, "alipay");
|
|
1416
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
1417
|
+
const challengeFile = (0, import_path2.join)(dir, `402_${reqId}.txt`);
|
|
1418
|
+
await (0, import_promises.writeFile)(challengeFile, paymentNeededHeader, "utf-8");
|
|
1419
|
+
const payArgs = [
|
|
1420
|
+
"402-buyer-pay",
|
|
1421
|
+
"-f",
|
|
1422
|
+
challengeFile,
|
|
1423
|
+
"-r",
|
|
1424
|
+
resourceUrl,
|
|
1425
|
+
"-s",
|
|
1426
|
+
this.sessionId,
|
|
1427
|
+
"-i",
|
|
1428
|
+
intentSummary,
|
|
1429
|
+
"-w",
|
|
1430
|
+
this.framework
|
|
1431
|
+
];
|
|
1432
|
+
if (opts.method) payArgs.push("-m", opts.method);
|
|
1433
|
+
if (opts.data) payArgs.push("-d", opts.data);
|
|
1434
|
+
const payRun = await this.runner(payArgs, { onLine, signal, step: "402-buyer-pay", flow });
|
|
1435
|
+
const tradeNo = parseTradeNo(payRun.lines);
|
|
1436
|
+
if (!tradeNo) {
|
|
1437
|
+
throw new AlipayProtocolError("402-buyer-pay did not return a tradeNo");
|
|
1438
|
+
}
|
|
1439
|
+
assertTradeNo(tradeNo);
|
|
1440
|
+
const { paymentUrl, shortenUrl } = parsePaymentUrl(payRun.lines);
|
|
1441
|
+
if (paymentUrl) {
|
|
1442
|
+
alipayLog.info("flow.pending", { flow, tradeNo, ms: this.now() - flowStart });
|
|
1443
|
+
opts.onPaymentPending?.({ paymentUrl, shortenUrl, tradeNo });
|
|
1444
|
+
}
|
|
1445
|
+
const pendingAt = this.now();
|
|
1446
|
+
const windowMs = (requirement.maxTimeoutSeconds ?? 30 * 60) * 1e3;
|
|
1447
|
+
const deadline = this.now() + (opts.timeoutMs ?? windowMs);
|
|
1448
|
+
const poll = await pollUntil(tradeNo, resourceUrl, {
|
|
1449
|
+
// No onLine: the status-poll output embeds the resource and must not reach
|
|
1450
|
+
// the log stream — the body is surfaced via the return value below (§9.3).
|
|
1451
|
+
deadline,
|
|
1452
|
+
signal,
|
|
1453
|
+
runner: this.runner,
|
|
1454
|
+
now: this.now,
|
|
1455
|
+
// Re-fetch the resource the same way it was paid (POST + body), else the
|
|
1456
|
+
// status poll defaults to GET and 404s on a POST-only `/execute`.
|
|
1457
|
+
method: opts.method,
|
|
1458
|
+
data: opts.data
|
|
1459
|
+
});
|
|
1460
|
+
alipayLog.info("flow.settled", { flow, tradeNo, ms: this.now() - pendingAt });
|
|
1461
|
+
const body = extractBody(poll.lines);
|
|
1462
|
+
void this.runner(["402-buyer-fulfillment-ack", "-t", tradeNo], {
|
|
1463
|
+
onLine,
|
|
1464
|
+
signal,
|
|
1465
|
+
step: "402-buyer-fulfillment-ack",
|
|
1466
|
+
flow
|
|
1467
|
+
}).catch(() => void 0);
|
|
1468
|
+
return { body, payment: { tradeNo, outTradeNo: reqId }, media };
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// src/client/alipay/router.ts
|
|
1473
|
+
init_cjs_shims();
|
|
1474
|
+
var ALIPAY_RAIL = "alipay";
|
|
1475
|
+
function railOf(req) {
|
|
1476
|
+
if (req.scheme === "alipay-aipay" || req.network === ALIPAY_RAIL) return ALIPAY_RAIL;
|
|
1477
|
+
return networkToChainName(req.network) ?? req.network;
|
|
1478
|
+
}
|
|
1479
|
+
function findRail(accepts, rail) {
|
|
1480
|
+
const requirement = accepts.find((r) => railOf(r) === rail);
|
|
1481
|
+
return requirement ? { rail, requirement } : null;
|
|
1482
|
+
}
|
|
1483
|
+
function selectRail(input) {
|
|
1484
|
+
const { serverAccepts, explicitRail, preference, availability } = input;
|
|
1485
|
+
if (!serverAccepts || serverAccepts.length === 0) {
|
|
1486
|
+
throw new UnsupportedRailError(explicitRail ?? "unknown", "Server offered no payment options");
|
|
1487
|
+
}
|
|
1488
|
+
if (explicitRail) {
|
|
1489
|
+
const hit = findRail(serverAccepts, explicitRail);
|
|
1490
|
+
if (!hit) {
|
|
1491
|
+
const offered = serverAccepts.map(railOf);
|
|
1492
|
+
throw new UnsupportedRailError(
|
|
1493
|
+
explicitRail,
|
|
1494
|
+
`Server doesn't accept rail '${explicitRail}'. Offered: ${offered.join(", ")}`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
return hit;
|
|
1498
|
+
}
|
|
1499
|
+
if (preference) {
|
|
1500
|
+
for (const pref of preference) {
|
|
1501
|
+
const hit = findRail(serverAccepts, pref);
|
|
1502
|
+
if (hit) return hit;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (availability?.evmReady) {
|
|
1506
|
+
const evm = serverAccepts.find((r) => railOf(r) !== ALIPAY_RAIL);
|
|
1507
|
+
if (evm) return { rail: railOf(evm), requirement: evm };
|
|
1508
|
+
}
|
|
1509
|
+
if (availability?.alipayReady) {
|
|
1510
|
+
const hit = findRail(serverAccepts, ALIPAY_RAIL);
|
|
1511
|
+
if (hit) return hit;
|
|
1512
|
+
}
|
|
1513
|
+
return { rail: railOf(serverAccepts[0]), requirement: serverAccepts[0] };
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/client/types.ts
|
|
1517
|
+
init_cjs_shims();
|
|
1518
|
+
|
|
1519
|
+
// src/client/node/index.ts
|
|
591
1520
|
var DEFAULT_CONFIG = {
|
|
592
1521
|
chain: "base",
|
|
593
1522
|
limits: {
|
|
@@ -600,15 +1529,24 @@ var MoltsPayClient = class {
|
|
|
600
1529
|
config;
|
|
601
1530
|
walletData = null;
|
|
602
1531
|
wallet = null;
|
|
1532
|
+
signer = null;
|
|
603
1533
|
todaySpending = 0;
|
|
604
1534
|
lastSpendingReset = 0;
|
|
1535
|
+
railPreference;
|
|
1536
|
+
alipaySessionId;
|
|
605
1537
|
constructor(options = {}) {
|
|
606
|
-
this.configDir = options.configDir || (0,
|
|
1538
|
+
this.configDir = options.configDir || (0, import_path3.join)((0, import_os3.homedir)(), ".moltspay");
|
|
607
1539
|
this.config = this.loadConfig();
|
|
1540
|
+
this.railPreference = options.railPreference ?? this.config.railPreference;
|
|
1541
|
+
this.alipaySessionId = options.alipaySessionId;
|
|
608
1542
|
this.walletData = this.loadWallet();
|
|
609
1543
|
this.loadSpending();
|
|
610
1544
|
if (this.walletData) {
|
|
611
|
-
this.wallet = new
|
|
1545
|
+
this.wallet = new import_ethers2.Wallet(this.walletData.privateKey);
|
|
1546
|
+
const configDir = this.configDir;
|
|
1547
|
+
this.signer = new NodeSigner(this.wallet, {
|
|
1548
|
+
getSolanaKeypair: () => loadSolanaWallet(configDir)
|
|
1549
|
+
});
|
|
612
1550
|
}
|
|
613
1551
|
}
|
|
614
1552
|
/**
|
|
@@ -678,6 +1616,9 @@ var MoltsPayClient = class {
|
|
|
678
1616
|
* @param options - Payment options (token selection)
|
|
679
1617
|
*/
|
|
680
1618
|
async pay(serverUrl, service, params, options = {}) {
|
|
1619
|
+
if (options.rail === ALIPAY_RAIL) {
|
|
1620
|
+
return this.payViaAlipay(serverUrl, service, params, options);
|
|
1621
|
+
}
|
|
681
1622
|
if (!this.wallet || !this.walletData) {
|
|
682
1623
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
683
1624
|
}
|
|
@@ -736,20 +1677,6 @@ var MoltsPayClient = class {
|
|
|
736
1677
|
} catch {
|
|
737
1678
|
throw new Error("Invalid x-payment-required header");
|
|
738
1679
|
}
|
|
739
|
-
const networkToChainName = (network2) => {
|
|
740
|
-
if (network2 === "solana:mainnet") return "solana";
|
|
741
|
-
if (network2 === "solana:devnet") return "solana_devnet";
|
|
742
|
-
const match = network2.match(/^eip155:(\d+)$/);
|
|
743
|
-
if (!match) return null;
|
|
744
|
-
const chainId = parseInt(match[1]);
|
|
745
|
-
if (chainId === 8453) return "base";
|
|
746
|
-
if (chainId === 137) return "polygon";
|
|
747
|
-
if (chainId === 84532) return "base_sepolia";
|
|
748
|
-
if (chainId === 42431) return "tempo_moderato";
|
|
749
|
-
if (chainId === 56) return "bnb";
|
|
750
|
-
if (chainId === 97) return "bnb_testnet";
|
|
751
|
-
return null;
|
|
752
|
-
};
|
|
753
1680
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
754
1681
|
const userSpecifiedChain = options.chain;
|
|
755
1682
|
let selectedChain;
|
|
@@ -883,6 +1810,76 @@ Please specify: --chain <chain_name>`
|
|
|
883
1810
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
884
1811
|
return result.result || result;
|
|
885
1812
|
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Pay for a service over the Alipay fiat rail (2.0.0).
|
|
1815
|
+
*
|
|
1816
|
+
* Unlike the crypto path this needs no EVM wallet — it shells out to
|
|
1817
|
+
* alipay-bot via {@link AlipayClient}. Flow: hit the resource with no
|
|
1818
|
+
* payment to get the 402 challenge, confirm the server actually offers the
|
|
1819
|
+
* alipay rail (selectRail), then run the 8-step state machine and return the
|
|
1820
|
+
* resource body.
|
|
1821
|
+
*/
|
|
1822
|
+
async payViaAlipay(serverUrl, service, params, options) {
|
|
1823
|
+
const flow = this.alipaySessionId;
|
|
1824
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
1825
|
+
try {
|
|
1826
|
+
const services = await timeStep(
|
|
1827
|
+
"discover-services",
|
|
1828
|
+
flow,
|
|
1829
|
+
() => this.getServices(serverUrl)
|
|
1830
|
+
);
|
|
1831
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
1832
|
+
if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
1833
|
+
} catch {
|
|
1834
|
+
}
|
|
1835
|
+
const requestBody = options.rawData ? { service, ...params } : { service, params };
|
|
1836
|
+
const bodyJson = JSON.stringify(requestBody);
|
|
1837
|
+
const res = await timeStep(
|
|
1838
|
+
"challenge-402",
|
|
1839
|
+
flow,
|
|
1840
|
+
() => fetch(executeUrl, {
|
|
1841
|
+
method: "POST",
|
|
1842
|
+
headers: { "Content-Type": "application/json" },
|
|
1843
|
+
body: bodyJson
|
|
1844
|
+
})
|
|
1845
|
+
);
|
|
1846
|
+
if (res.status !== 402) {
|
|
1847
|
+
const data = await res.json().catch(() => ({}));
|
|
1848
|
+
if (res.ok && data.result) return data.result;
|
|
1849
|
+
throw new Error(data.error || `Expected 402, got ${res.status}`);
|
|
1850
|
+
}
|
|
1851
|
+
const header = res.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1852
|
+
if (!header) throw new Error("Missing x-payment-required header on 402");
|
|
1853
|
+
const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
|
|
1854
|
+
const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
|
|
1855
|
+
const { requirement } = selectRail({
|
|
1856
|
+
serverAccepts: accepts,
|
|
1857
|
+
explicitRail: ALIPAY_RAIL,
|
|
1858
|
+
preference: this.railPreference,
|
|
1859
|
+
availability: { evmReady: this.isInitialized }
|
|
1860
|
+
});
|
|
1861
|
+
const onLine = options.onLine ?? ((line) => process.stdout.write(line + "\n"));
|
|
1862
|
+
const alipay = new AlipayClient({
|
|
1863
|
+
sessionId: this.alipaySessionId,
|
|
1864
|
+
configDir: this.configDir
|
|
1865
|
+
});
|
|
1866
|
+
const result = await alipay.pay402({
|
|
1867
|
+
resourceUrl: executeUrl,
|
|
1868
|
+
requirement,
|
|
1869
|
+
method: "POST",
|
|
1870
|
+
data: bodyJson,
|
|
1871
|
+
onLine,
|
|
1872
|
+
onPaymentPending: options.onPaymentPending,
|
|
1873
|
+
timeoutMs: options.timeoutMs,
|
|
1874
|
+
signal: options.signal
|
|
1875
|
+
});
|
|
1876
|
+
try {
|
|
1877
|
+
const json = JSON.parse(result.body);
|
|
1878
|
+
return json.result ?? json;
|
|
1879
|
+
} catch {
|
|
1880
|
+
return { body: result.body, payment: result.payment, media: result.media };
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
886
1883
|
/**
|
|
887
1884
|
* Handle MPP (Machine Payments Protocol) payment flow
|
|
888
1885
|
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
@@ -978,14 +1975,14 @@ Please specify: --chain <chain_name>`
|
|
|
978
1975
|
async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
|
|
979
1976
|
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
980
1977
|
const tokenConfig = chain.tokens[token];
|
|
981
|
-
const provider = new
|
|
1978
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
|
|
982
1979
|
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
983
1980
|
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
984
1981
|
if (allowance < amountWeiCheck) {
|
|
985
1982
|
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
986
|
-
const minGasBalance =
|
|
1983
|
+
const minGasBalance = import_ethers2.ethers.parseEther("0.0005");
|
|
987
1984
|
if (nativeBalance < minGasBalance) {
|
|
988
|
-
const nativeBNB = parseFloat(
|
|
1985
|
+
const nativeBNB = parseFloat(import_ethers2.ethers.formatEther(nativeBalance)).toFixed(4);
|
|
989
1986
|
const isTestnet = chainName === "bnb_testnet";
|
|
990
1987
|
if (isTestnet) {
|
|
991
1988
|
throw new Error(
|
|
@@ -1019,35 +2016,21 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1019
2016
|
);
|
|
1020
2017
|
}
|
|
1021
2018
|
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
1022
|
-
const
|
|
2019
|
+
const intentNonce = Date.now();
|
|
2020
|
+
const intentDeadline = Date.now() + 36e5;
|
|
2021
|
+
const envelope = buildBnbIntentTypedData({
|
|
1023
2022
|
from: this.wallet.address,
|
|
1024
2023
|
to,
|
|
1025
2024
|
amount: amountWei,
|
|
1026
|
-
|
|
2025
|
+
tokenAddress: tokenConfig.address,
|
|
1027
2026
|
service,
|
|
1028
|
-
nonce:
|
|
1029
|
-
|
|
1030
|
-
deadline: Date.now() + 36e5
|
|
1031
|
-
// 1 hour
|
|
1032
|
-
};
|
|
1033
|
-
const domain = {
|
|
1034
|
-
name: "MoltsPay",
|
|
1035
|
-
version: "1",
|
|
2027
|
+
nonce: intentNonce,
|
|
2028
|
+
deadline: intentDeadline,
|
|
1036
2029
|
chainId: chain.chainId
|
|
1037
|
-
};
|
|
1038
|
-
const types = {
|
|
1039
|
-
PaymentIntent: [
|
|
1040
|
-
{ name: "from", type: "address" },
|
|
1041
|
-
{ name: "to", type: "address" },
|
|
1042
|
-
{ name: "amount", type: "uint256" },
|
|
1043
|
-
{ name: "token", type: "address" },
|
|
1044
|
-
{ name: "service", type: "string" },
|
|
1045
|
-
{ name: "nonce", type: "uint256" },
|
|
1046
|
-
{ name: "deadline", type: "uint256" }
|
|
1047
|
-
]
|
|
1048
|
-
};
|
|
2030
|
+
});
|
|
1049
2031
|
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
1050
|
-
const signature = await this.
|
|
2032
|
+
const signature = await this.signer.signTypedData(envelope);
|
|
2033
|
+
const intent = envelope.message;
|
|
1051
2034
|
const network = `eip155:${chain.chainId}`;
|
|
1052
2035
|
const payload = {
|
|
1053
2036
|
x402Version: 2,
|
|
@@ -1108,11 +2091,11 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1108
2091
|
throw new Error("Missing payTo address in payment requirements");
|
|
1109
2092
|
}
|
|
1110
2093
|
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
1111
|
-
const feePayerPubkey = solanaFeePayer ? new
|
|
2094
|
+
const feePayerPubkey = solanaFeePayer ? new import_web35.PublicKey(solanaFeePayer) : void 0;
|
|
1112
2095
|
if (feePayerPubkey) {
|
|
1113
2096
|
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
1114
2097
|
}
|
|
1115
|
-
const recipientPubkey = new
|
|
2098
|
+
const recipientPubkey = new import_web35.PublicKey(requirements.payTo);
|
|
1116
2099
|
const transaction = await createSolanaPaymentTransaction(
|
|
1117
2100
|
solanaWallet.publicKey,
|
|
1118
2101
|
recipientPubkey,
|
|
@@ -1121,12 +2104,11 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1121
2104
|
feePayerPubkey
|
|
1122
2105
|
// Optional fee payer for gasless mode
|
|
1123
2106
|
);
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
}
|
|
1129
|
-
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
2107
|
+
const unsignedBase64 = transaction.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
|
|
2108
|
+
const signedTx = await this.signer.signSolanaTransaction({
|
|
2109
|
+
transactionBase64: unsignedBase64,
|
|
2110
|
+
partialSign: !!feePayerPubkey
|
|
2111
|
+
});
|
|
1130
2112
|
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
1131
2113
|
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
1132
2114
|
const payload = {
|
|
@@ -1173,7 +2155,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1173
2155
|
* Check ERC20 allowance for a spender
|
|
1174
2156
|
*/
|
|
1175
2157
|
async checkAllowance(tokenAddress, spender, provider) {
|
|
1176
|
-
const contract = new
|
|
2158
|
+
const contract = new import_ethers2.ethers.Contract(
|
|
1177
2159
|
tokenAddress,
|
|
1178
2160
|
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
1179
2161
|
provider
|
|
@@ -1184,41 +2166,29 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1184
2166
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
1185
2167
|
* This only signs - no on-chain transaction, no gas needed.
|
|
1186
2168
|
* Supports both USDC and USDT.
|
|
2169
|
+
*
|
|
2170
|
+
* Delegates typed-data construction to `core/eip3009.ts` and the signature
|
|
2171
|
+
* itself to `this.signer`. That way Web Client (Phase 4) can reuse the same
|
|
2172
|
+
* flow with an EIP-1193 signer without duplicating typed-data layout.
|
|
1187
2173
|
*/
|
|
1188
2174
|
async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
|
|
1189
|
-
const validAfter = 0;
|
|
1190
|
-
const validBefore = Math.floor(Date.now() / 1e3) + 3600;
|
|
1191
|
-
const nonce = import_ethers.ethers.hexlify(import_ethers.ethers.randomBytes(32));
|
|
1192
2175
|
const tokenConfig = chain.tokens[token];
|
|
1193
2176
|
const value = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
1194
|
-
const
|
|
2177
|
+
const nonce = import_ethers2.ethers.hexlify(import_ethers2.ethers.randomBytes(32));
|
|
2178
|
+
const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
|
|
2179
|
+
const tokenVersion = domainOverride?.version || "2";
|
|
2180
|
+
const envelope = buildEIP3009TypedData({
|
|
1195
2181
|
from: this.wallet.address,
|
|
1196
2182
|
to,
|
|
1197
2183
|
value,
|
|
1198
|
-
|
|
1199
|
-
validBefore: validBefore.toString(),
|
|
1200
|
-
nonce
|
|
1201
|
-
};
|
|
1202
|
-
const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
|
|
1203
|
-
const tokenVersion = domainOverride?.version || "2";
|
|
1204
|
-
const domain = {
|
|
1205
|
-
name: tokenName,
|
|
1206
|
-
version: tokenVersion,
|
|
2184
|
+
nonce,
|
|
1207
2185
|
chainId: chain.chainId,
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
{ name: "value", type: "uint256" },
|
|
1215
|
-
{ name: "validAfter", type: "uint256" },
|
|
1216
|
-
{ name: "validBefore", type: "uint256" },
|
|
1217
|
-
{ name: "nonce", type: "bytes32" }
|
|
1218
|
-
]
|
|
1219
|
-
};
|
|
1220
|
-
const signature = await this.wallet.signTypedData(domain, types, authorization);
|
|
1221
|
-
return { authorization, signature };
|
|
2186
|
+
tokenAddress: tokenConfig.address,
|
|
2187
|
+
tokenName,
|
|
2188
|
+
tokenVersion
|
|
2189
|
+
});
|
|
2190
|
+
const signature = await this.signer.signTypedData(envelope);
|
|
2191
|
+
return { authorization: envelope.message, signature };
|
|
1222
2192
|
}
|
|
1223
2193
|
/**
|
|
1224
2194
|
* Check spending limits
|
|
@@ -1250,7 +2220,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1250
2220
|
}
|
|
1251
2221
|
// --- Config & Wallet Management ---
|
|
1252
2222
|
loadConfig() {
|
|
1253
|
-
const configPath = (0,
|
|
2223
|
+
const configPath = (0, import_path3.join)(this.configDir, "config.json");
|
|
1254
2224
|
if ((0, import_fs2.existsSync)(configPath)) {
|
|
1255
2225
|
const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
|
|
1256
2226
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
@@ -1259,14 +2229,14 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1259
2229
|
}
|
|
1260
2230
|
saveConfig() {
|
|
1261
2231
|
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1262
|
-
const configPath = (0,
|
|
2232
|
+
const configPath = (0, import_path3.join)(this.configDir, "config.json");
|
|
1263
2233
|
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
1264
2234
|
}
|
|
1265
2235
|
/**
|
|
1266
2236
|
* Load spending data from disk
|
|
1267
2237
|
*/
|
|
1268
2238
|
loadSpending() {
|
|
1269
|
-
const spendingPath = (0,
|
|
2239
|
+
const spendingPath = (0, import_path3.join)(this.configDir, "spending.json");
|
|
1270
2240
|
if ((0, import_fs2.existsSync)(spendingPath)) {
|
|
1271
2241
|
try {
|
|
1272
2242
|
const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
|
|
@@ -1289,7 +2259,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1289
2259
|
*/
|
|
1290
2260
|
saveSpending() {
|
|
1291
2261
|
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1292
|
-
const spendingPath = (0,
|
|
2262
|
+
const spendingPath = (0, import_path3.join)(this.configDir, "spending.json");
|
|
1293
2263
|
const data = {
|
|
1294
2264
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
1295
2265
|
amount: this.todaySpending,
|
|
@@ -1298,7 +2268,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1298
2268
|
(0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
1299
2269
|
}
|
|
1300
2270
|
loadWallet() {
|
|
1301
|
-
const walletPath = (0,
|
|
2271
|
+
const walletPath = (0, import_path3.join)(this.configDir, "wallet.json");
|
|
1302
2272
|
if ((0, import_fs2.existsSync)(walletPath)) {
|
|
1303
2273
|
if (process.platform !== "win32") {
|
|
1304
2274
|
try {
|
|
@@ -1322,13 +2292,13 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1322
2292
|
*/
|
|
1323
2293
|
static init(configDir, options) {
|
|
1324
2294
|
(0, import_fs2.mkdirSync)(configDir, { recursive: true });
|
|
1325
|
-
const wallet =
|
|
2295
|
+
const wallet = import_ethers2.Wallet.createRandom();
|
|
1326
2296
|
const walletData = {
|
|
1327
2297
|
address: wallet.address,
|
|
1328
2298
|
privateKey: wallet.privateKey,
|
|
1329
2299
|
createdAt: Date.now()
|
|
1330
2300
|
};
|
|
1331
|
-
const walletPath = (0,
|
|
2301
|
+
const walletPath = (0, import_path3.join)(configDir, "wallet.json");
|
|
1332
2302
|
(0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
1333
2303
|
const config = {
|
|
1334
2304
|
chain: options.chain,
|
|
@@ -1337,7 +2307,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1337
2307
|
maxPerDay: options.maxPerDay
|
|
1338
2308
|
}
|
|
1339
2309
|
};
|
|
1340
|
-
const configPath = (0,
|
|
2310
|
+
const configPath = (0, import_path3.join)(configDir, "config.json");
|
|
1341
2311
|
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
1342
2312
|
return { address: wallet.address, configDir };
|
|
1343
2313
|
}
|
|
@@ -1354,17 +2324,17 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1354
2324
|
} catch {
|
|
1355
2325
|
throw new Error(`Unknown chain: ${this.config.chain}`);
|
|
1356
2326
|
}
|
|
1357
|
-
const provider = new
|
|
2327
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
|
|
1358
2328
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
1359
2329
|
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
1360
2330
|
provider.getBalance(this.wallet.address),
|
|
1361
|
-
new
|
|
1362
|
-
new
|
|
2331
|
+
new import_ethers2.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2332
|
+
new import_ethers2.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1363
2333
|
]);
|
|
1364
2334
|
return {
|
|
1365
|
-
usdc: parseFloat(
|
|
1366
|
-
usdt: parseFloat(
|
|
1367
|
-
native: parseFloat(
|
|
2335
|
+
usdc: parseFloat(import_ethers2.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
2336
|
+
usdt: parseFloat(import_ethers2.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
2337
|
+
native: parseFloat(import_ethers2.ethers.formatEther(nativeBalance))
|
|
1368
2338
|
};
|
|
1369
2339
|
}
|
|
1370
2340
|
/**
|
|
@@ -1387,38 +2357,38 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1387
2357
|
supportedChains.map(async (chainName) => {
|
|
1388
2358
|
try {
|
|
1389
2359
|
const chain = getChain(chainName);
|
|
1390
|
-
const provider = new
|
|
2360
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
|
|
1391
2361
|
if (chainName === "tempo_moderato") {
|
|
1392
2362
|
const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
|
|
1393
2363
|
provider.getBalance(this.wallet.address),
|
|
1394
|
-
new
|
|
1395
|
-
new
|
|
1396
|
-
new
|
|
1397
|
-
new
|
|
2364
|
+
new import_ethers2.ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2365
|
+
new import_ethers2.ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2366
|
+
new import_ethers2.ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2367
|
+
new import_ethers2.ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1398
2368
|
]);
|
|
1399
2369
|
results[chainName] = {
|
|
1400
|
-
usdc: parseFloat(
|
|
2370
|
+
usdc: parseFloat(import_ethers2.ethers.formatUnits(pathUSD, 6)),
|
|
1401
2371
|
// pathUSD as default USDC
|
|
1402
|
-
usdt: parseFloat(
|
|
2372
|
+
usdt: parseFloat(import_ethers2.ethers.formatUnits(alphaUSD, 6)),
|
|
1403
2373
|
// alphaUSD as default USDT
|
|
1404
|
-
native: parseFloat(
|
|
2374
|
+
native: parseFloat(import_ethers2.ethers.formatEther(nativeBalance)),
|
|
1405
2375
|
tempo: {
|
|
1406
|
-
pathUSD: parseFloat(
|
|
1407
|
-
alphaUSD: parseFloat(
|
|
1408
|
-
betaUSD: parseFloat(
|
|
1409
|
-
thetaUSD: parseFloat(
|
|
2376
|
+
pathUSD: parseFloat(import_ethers2.ethers.formatUnits(pathUSD, 6)),
|
|
2377
|
+
alphaUSD: parseFloat(import_ethers2.ethers.formatUnits(alphaUSD, 6)),
|
|
2378
|
+
betaUSD: parseFloat(import_ethers2.ethers.formatUnits(betaUSD, 6)),
|
|
2379
|
+
thetaUSD: parseFloat(import_ethers2.ethers.formatUnits(thetaUSD, 6))
|
|
1410
2380
|
}
|
|
1411
2381
|
};
|
|
1412
2382
|
} else {
|
|
1413
2383
|
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
1414
2384
|
provider.getBalance(this.wallet.address),
|
|
1415
|
-
new
|
|
1416
|
-
new
|
|
2385
|
+
new import_ethers2.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2386
|
+
new import_ethers2.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1417
2387
|
]);
|
|
1418
2388
|
results[chainName] = {
|
|
1419
|
-
usdc: parseFloat(
|
|
1420
|
-
usdt: parseFloat(
|
|
1421
|
-
native: parseFloat(
|
|
2389
|
+
usdc: parseFloat(import_ethers2.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
2390
|
+
usdt: parseFloat(import_ethers2.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
2391
|
+
native: parseFloat(import_ethers2.ethers.formatEther(nativeBalance))
|
|
1422
2392
|
};
|
|
1423
2393
|
}
|
|
1424
2394
|
} catch (err) {
|
|
@@ -1776,16 +2746,40 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
1776
2746
|
|
|
1777
2747
|
// src/facilitators/tempo.ts
|
|
1778
2748
|
init_cjs_shims();
|
|
2749
|
+
var import_ethers3 = require("ethers");
|
|
1779
2750
|
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
2751
|
+
var TIP20_PERMIT_ABI = [
|
|
2752
|
+
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
|
2753
|
+
"function transferFrom(address from, address to, uint256 value) returns (bool)"
|
|
2754
|
+
];
|
|
1780
2755
|
var TempoFacilitator = class extends BaseFacilitator {
|
|
1781
2756
|
name = "tempo";
|
|
1782
2757
|
displayName = "Tempo Testnet";
|
|
1783
2758
|
supportedNetworks = ["eip155:42431"];
|
|
1784
2759
|
// Tempo Moderato
|
|
1785
2760
|
rpcUrl;
|
|
2761
|
+
settlerWallet = null;
|
|
1786
2762
|
constructor() {
|
|
1787
2763
|
super();
|
|
1788
2764
|
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
2765
|
+
const settlerKey = process.env.TEMPO_SETTLER_KEY;
|
|
2766
|
+
if (settlerKey) {
|
|
2767
|
+
try {
|
|
2768
|
+
const provider = new import_ethers3.ethers.JsonRpcProvider(this.rpcUrl);
|
|
2769
|
+
this.settlerWallet = new import_ethers3.ethers.Wallet(settlerKey, provider);
|
|
2770
|
+
} catch (err) {
|
|
2771
|
+
console.warn("[TempoFacilitator] Invalid TEMPO_SETTLER_KEY, permit settlement disabled:", err);
|
|
2772
|
+
this.settlerWallet = null;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Settler EOA address advertised to clients via `X-Payment-Required.extra.tempoSpender`.
|
|
2778
|
+
* Web Client uses this as the `spender` field in the signed EIP-2612 Permit.
|
|
2779
|
+
* Returns null if no TEMPO_SETTLER_KEY is configured — permit settlement unavailable.
|
|
2780
|
+
*/
|
|
2781
|
+
getSpenderAddress() {
|
|
2782
|
+
return this.settlerWallet?.address ?? null;
|
|
1789
2783
|
}
|
|
1790
2784
|
async healthCheck() {
|
|
1791
2785
|
const start = Date.now();
|
|
@@ -1810,7 +2804,45 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1810
2804
|
return { healthy: false, error: String(error) };
|
|
1811
2805
|
}
|
|
1812
2806
|
}
|
|
1813
|
-
async verify(paymentPayload, requirements) {
|
|
2807
|
+
async verify(paymentPayload, requirements) {
|
|
2808
|
+
const inner = paymentPayload.payload;
|
|
2809
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
2810
|
+
return this.verifyPermit(inner, requirements);
|
|
2811
|
+
}
|
|
2812
|
+
return this.verifyTxHash(paymentPayload, requirements);
|
|
2813
|
+
}
|
|
2814
|
+
/**
|
|
2815
|
+
* Structural validation of an EIP-2612 permit payload. Does NOT submit
|
|
2816
|
+
* anything on-chain — actual submission happens in settlePermit().
|
|
2817
|
+
*/
|
|
2818
|
+
async verifyPermit(payload, requirements) {
|
|
2819
|
+
if (!this.settlerWallet) {
|
|
2820
|
+
return { valid: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
2821
|
+
}
|
|
2822
|
+
const p = payload.permit;
|
|
2823
|
+
if (!p || !p.owner || !p.spender || !p.value || !p.deadline) {
|
|
2824
|
+
return { valid: false, error: "Invalid permit payload: missing fields" };
|
|
2825
|
+
}
|
|
2826
|
+
if (p.spender.toLowerCase() !== this.settlerWallet.address.toLowerCase()) {
|
|
2827
|
+
return {
|
|
2828
|
+
valid: false,
|
|
2829
|
+
error: `Permit spender ${p.spender} does not match configured settler ${this.settlerWallet.address}`
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
const deadline = BigInt(p.deadline);
|
|
2833
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
2834
|
+
if (deadline <= now) {
|
|
2835
|
+
return { valid: false, error: "Permit deadline has expired" };
|
|
2836
|
+
}
|
|
2837
|
+
if (BigInt(p.value) < BigInt(requirements.amount || "0")) {
|
|
2838
|
+
return {
|
|
2839
|
+
valid: false,
|
|
2840
|
+
error: `Permit value ${p.value} is less than required ${requirements.amount}`
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
return { valid: true, details: { scheme: "permit", owner: p.owner } };
|
|
2844
|
+
}
|
|
2845
|
+
async verifyTxHash(paymentPayload, requirements) {
|
|
1814
2846
|
try {
|
|
1815
2847
|
const tempoPayload = paymentPayload.payload;
|
|
1816
2848
|
if (!tempoPayload?.txHash) {
|
|
@@ -1868,7 +2900,11 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1868
2900
|
}
|
|
1869
2901
|
}
|
|
1870
2902
|
async settle(paymentPayload, requirements) {
|
|
1871
|
-
const
|
|
2903
|
+
const inner = paymentPayload.payload;
|
|
2904
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
2905
|
+
return this.settlePermit(inner, requirements);
|
|
2906
|
+
}
|
|
2907
|
+
const verifyResult = await this.verifyTxHash(paymentPayload, requirements);
|
|
1872
2908
|
if (!verifyResult.valid) {
|
|
1873
2909
|
return { success: false, error: verifyResult.error };
|
|
1874
2910
|
}
|
|
@@ -1879,6 +2915,52 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1879
2915
|
status: "settled"
|
|
1880
2916
|
};
|
|
1881
2917
|
}
|
|
2918
|
+
/**
|
|
2919
|
+
* EIP-2612 permit settlement path. Submits two transactions on Tempo:
|
|
2920
|
+
* 1. pathUSD.permit(owner, spender=settler, value, deadline, v, r, s)
|
|
2921
|
+
* 2. pathUSD.transferFrom(owner, payTo, value)
|
|
2922
|
+
*
|
|
2923
|
+
* The settler EOA pays Tempo gas (via the TIP-20 `feeToken` mechanism — no
|
|
2924
|
+
* native tTEMPO required; any held TIP-20 token balance covers fees).
|
|
2925
|
+
*/
|
|
2926
|
+
async settlePermit(payload, requirements) {
|
|
2927
|
+
if (!this.settlerWallet) {
|
|
2928
|
+
return { success: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
2929
|
+
}
|
|
2930
|
+
if (!requirements.asset || !requirements.payTo) {
|
|
2931
|
+
return { success: false, error: "Missing asset or payTo in requirements" };
|
|
2932
|
+
}
|
|
2933
|
+
const verifyResult = await this.verifyPermit(payload, requirements);
|
|
2934
|
+
if (!verifyResult.valid) {
|
|
2935
|
+
return { success: false, error: verifyResult.error };
|
|
2936
|
+
}
|
|
2937
|
+
const token = new import_ethers3.ethers.Contract(requirements.asset, TIP20_PERMIT_ABI, this.settlerWallet);
|
|
2938
|
+
const p = payload.permit;
|
|
2939
|
+
try {
|
|
2940
|
+
const permitTx = await token.permit(
|
|
2941
|
+
p.owner,
|
|
2942
|
+
p.spender,
|
|
2943
|
+
p.value,
|
|
2944
|
+
p.deadline,
|
|
2945
|
+
p.v,
|
|
2946
|
+
p.r,
|
|
2947
|
+
p.s
|
|
2948
|
+
);
|
|
2949
|
+
await permitTx.wait();
|
|
2950
|
+
const transferTx = await token.transferFrom(p.owner, requirements.payTo, p.value);
|
|
2951
|
+
await transferTx.wait();
|
|
2952
|
+
return {
|
|
2953
|
+
success: true,
|
|
2954
|
+
transaction: transferTx.hash,
|
|
2955
|
+
status: "settled"
|
|
2956
|
+
};
|
|
2957
|
+
} catch (err) {
|
|
2958
|
+
return {
|
|
2959
|
+
success: false,
|
|
2960
|
+
error: `Tempo permit settlement failed: ${err.message}`
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
1882
2964
|
async getTransactionReceipt(txHash) {
|
|
1883
2965
|
const response = await fetch(this.rpcUrl, {
|
|
1884
2966
|
method: "POST",
|
|
@@ -2122,12 +3204,12 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
2122
3204
|
return this.spenderAddress;
|
|
2123
3205
|
}
|
|
2124
3206
|
async getServerAddress() {
|
|
2125
|
-
const { ethers:
|
|
2126
|
-
const wallet = new
|
|
3207
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
3208
|
+
const wallet = new ethers5.Wallet(this.serverPrivateKey);
|
|
2127
3209
|
return wallet.address;
|
|
2128
3210
|
}
|
|
2129
3211
|
async recoverIntentSigner(intent, chainId) {
|
|
2130
|
-
const { ethers:
|
|
3212
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
2131
3213
|
const domain = {
|
|
2132
3214
|
...EIP712_DOMAIN,
|
|
2133
3215
|
chainId
|
|
@@ -2141,7 +3223,7 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
2141
3223
|
nonce: intent.nonce,
|
|
2142
3224
|
deadline: intent.deadline
|
|
2143
3225
|
};
|
|
2144
|
-
const recoveredAddress =
|
|
3226
|
+
const recoveredAddress = ethers5.verifyTypedData(
|
|
2145
3227
|
domain,
|
|
2146
3228
|
INTENT_TYPES,
|
|
2147
3229
|
message,
|
|
@@ -2185,10 +3267,10 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
2185
3267
|
return result.result || "0x0";
|
|
2186
3268
|
}
|
|
2187
3269
|
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
2188
|
-
const { ethers:
|
|
2189
|
-
const provider = new
|
|
2190
|
-
const wallet = new
|
|
2191
|
-
const tokenContract = new
|
|
3270
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
3271
|
+
const provider = new ethers5.JsonRpcProvider(rpcUrl);
|
|
3272
|
+
const wallet = new ethers5.Wallet(this.serverPrivateKey, provider);
|
|
3273
|
+
const tokenContract = new ethers5.Contract(token, [
|
|
2192
3274
|
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
2193
3275
|
], wallet);
|
|
2194
3276
|
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
@@ -2211,9 +3293,381 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
2211
3293
|
}
|
|
2212
3294
|
};
|
|
2213
3295
|
|
|
3296
|
+
// src/facilitators/alipay.ts
|
|
3297
|
+
init_cjs_shims();
|
|
3298
|
+
var import_node_crypto3 = __toESM(require("crypto"));
|
|
3299
|
+
|
|
3300
|
+
// src/facilitators/alipay/rsa2.ts
|
|
3301
|
+
init_cjs_shims();
|
|
3302
|
+
var import_node_crypto2 = __toESM(require("crypto"));
|
|
3303
|
+
function rsa2Sign(data, privateKeyPem) {
|
|
3304
|
+
const signer = import_node_crypto2.default.createSign("RSA-SHA256");
|
|
3305
|
+
signer.update(data, "utf-8");
|
|
3306
|
+
signer.end();
|
|
3307
|
+
return signer.sign(privateKeyPem, "base64");
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
// src/facilitators/alipay/encoding.ts
|
|
3311
|
+
init_cjs_shims();
|
|
3312
|
+
function base64url(input) {
|
|
3313
|
+
return Buffer.from(input, "utf-8").toString("base64url");
|
|
3314
|
+
}
|
|
3315
|
+
function decodeBase64UrlWithPadFix(input) {
|
|
3316
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
3317
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
3318
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
3319
|
+
}
|
|
3320
|
+
function toPem(key, kind) {
|
|
3321
|
+
const trimmed = key.trim();
|
|
3322
|
+
if (trimmed.includes("-----BEGIN")) return trimmed;
|
|
3323
|
+
const label = kind === "PRIVATE" ? "PRIVATE KEY" : "PUBLIC KEY";
|
|
3324
|
+
const body = trimmed.replace(/\s+/g, "").match(/.{1,64}/g)?.join("\n") ?? "";
|
|
3325
|
+
return `-----BEGIN ${label}-----
|
|
3326
|
+
${body}
|
|
3327
|
+
-----END ${label}-----
|
|
3328
|
+
`;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
// src/facilitators/alipay/openapi.ts
|
|
3332
|
+
init_cjs_shims();
|
|
3333
|
+
function formatAlipayTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
3334
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
3335
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
3336
|
+
}
|
|
3337
|
+
function buildSigningString(params) {
|
|
3338
|
+
return Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
3339
|
+
}
|
|
3340
|
+
function responseWrapperKey(method) {
|
|
3341
|
+
return `${method.replace(/\./g, "_")}_response`;
|
|
3342
|
+
}
|
|
3343
|
+
async function alipayOpenApiCall(method, bizContent, config) {
|
|
3344
|
+
const publicParams = {
|
|
3345
|
+
app_id: config.app_id,
|
|
3346
|
+
method,
|
|
3347
|
+
format: "JSON",
|
|
3348
|
+
charset: "utf-8",
|
|
3349
|
+
sign_type: config.sign_type ?? "RSA2",
|
|
3350
|
+
timestamp: formatAlipayTimestamp(),
|
|
3351
|
+
version: "1.0",
|
|
3352
|
+
biz_content: JSON.stringify(bizContent)
|
|
3353
|
+
};
|
|
3354
|
+
const signingString = buildSigningString(publicParams);
|
|
3355
|
+
const sign = rsa2Sign(signingString, config.private_key_pem);
|
|
3356
|
+
const body = new URLSearchParams({ ...publicParams, sign }).toString();
|
|
3357
|
+
const response = await fetch(config.gateway_url, {
|
|
3358
|
+
method: "POST",
|
|
3359
|
+
headers: {
|
|
3360
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
|
|
3361
|
+
},
|
|
3362
|
+
body
|
|
3363
|
+
});
|
|
3364
|
+
if (!response.ok) {
|
|
3365
|
+
const text = await response.text().catch(() => "<unreadable>");
|
|
3366
|
+
throw new Error(
|
|
3367
|
+
`Alipay Open API HTTP ${response.status} for ${method}: ${text.slice(0, 500)}`
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
const json = await response.json();
|
|
3371
|
+
const wrapperKey = responseWrapperKey(method);
|
|
3372
|
+
const business = json[wrapperKey];
|
|
3373
|
+
if (business === void 0 || business === null || typeof business !== "object") {
|
|
3374
|
+
throw new Error(
|
|
3375
|
+
`Alipay Open API response missing "${wrapperKey}" wrapper for ${method}: ${JSON.stringify(json).slice(0, 500)}`
|
|
3376
|
+
);
|
|
3377
|
+
}
|
|
3378
|
+
return business;
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
// src/facilitators/alipay.ts
|
|
3382
|
+
var ALIPAY_NETWORK = "alipay";
|
|
3383
|
+
var ALIPAY_SCHEME = "alipay-aipay";
|
|
3384
|
+
var ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
|
|
3385
|
+
var ALIPAY_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
|
|
3386
|
+
var ALIPAY_PAY_BEFORE_MS = 30 * 60 * 1e3;
|
|
3387
|
+
var ALIPAY_SIGNING_FIELDS = [
|
|
3388
|
+
"amount",
|
|
3389
|
+
"currency",
|
|
3390
|
+
"goods_name",
|
|
3391
|
+
"out_trade_no",
|
|
3392
|
+
"pay_before",
|
|
3393
|
+
"resource_id",
|
|
3394
|
+
"seller_id",
|
|
3395
|
+
"service_id"
|
|
3396
|
+
];
|
|
3397
|
+
var AlipayFacilitator = class extends BaseFacilitator {
|
|
3398
|
+
name = "alipay";
|
|
3399
|
+
displayName = "Alipay AI \u6536";
|
|
3400
|
+
supportedNetworks = [ALIPAY_NETWORK];
|
|
3401
|
+
config;
|
|
3402
|
+
constructor(config) {
|
|
3403
|
+
super();
|
|
3404
|
+
this.config = {
|
|
3405
|
+
gateway_url: ALIPAY_GATEWAY_PROD,
|
|
3406
|
+
sign_type: "RSA2",
|
|
3407
|
+
...config
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Build the 402 challenge for a service: signs the 8-field payload with
|
|
3412
|
+
* RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
|
|
3413
|
+
* `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
|
|
3414
|
+
*/
|
|
3415
|
+
async createPaymentRequirements(opts) {
|
|
3416
|
+
if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
|
|
3417
|
+
throw new Error(
|
|
3418
|
+
`AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
const now = /* @__PURE__ */ new Date();
|
|
3422
|
+
const outTradeNo = opts.outTradeNo ?? generateOutTradeNo();
|
|
3423
|
+
const payBefore = formatPayBefore(now);
|
|
3424
|
+
const signedFields = {
|
|
3425
|
+
amount: opts.priceCny,
|
|
3426
|
+
currency: "CNY",
|
|
3427
|
+
goods_name: opts.goodsName,
|
|
3428
|
+
out_trade_no: outTradeNo,
|
|
3429
|
+
pay_before: payBefore,
|
|
3430
|
+
resource_id: opts.resourceId,
|
|
3431
|
+
seller_id: this.config.seller_id,
|
|
3432
|
+
service_id: opts.serviceId
|
|
3433
|
+
};
|
|
3434
|
+
const signingString = ALIPAY_SIGNING_FIELDS.map((k) => `${k}=${signedFields[k]}`).join("&");
|
|
3435
|
+
const seller_signature = rsa2Sign(signingString, this.config.private_key_pem);
|
|
3436
|
+
const challenge = {
|
|
3437
|
+
protocol: {
|
|
3438
|
+
out_trade_no: outTradeNo,
|
|
3439
|
+
amount: signedFields.amount,
|
|
3440
|
+
currency: signedFields.currency,
|
|
3441
|
+
resource_id: signedFields.resource_id,
|
|
3442
|
+
pay_before: payBefore,
|
|
3443
|
+
seller_signature,
|
|
3444
|
+
seller_sign_type: this.config.sign_type ?? "RSA2",
|
|
3445
|
+
seller_unique_id: this.config.seller_id
|
|
3446
|
+
},
|
|
3447
|
+
method: {
|
|
3448
|
+
seller_name: this.config.seller_name,
|
|
3449
|
+
seller_id: this.config.seller_id,
|
|
3450
|
+
seller_app_id: this.config.app_id,
|
|
3451
|
+
goods_name: signedFields.goods_name,
|
|
3452
|
+
seller_unique_id_key: "seller_id",
|
|
3453
|
+
service_id: signedFields.service_id
|
|
3454
|
+
}
|
|
3455
|
+
};
|
|
3456
|
+
const paymentNeededHeader = base64url(JSON.stringify(challenge));
|
|
3457
|
+
const x402Accepts = {
|
|
3458
|
+
scheme: ALIPAY_SCHEME,
|
|
3459
|
+
network: ALIPAY_NETWORK,
|
|
3460
|
+
asset: "CNY",
|
|
3461
|
+
amount: opts.priceCny,
|
|
3462
|
+
payTo: this.config.seller_id,
|
|
3463
|
+
maxTimeoutSeconds: ALIPAY_PAY_BEFORE_MS / 1e3,
|
|
3464
|
+
extra: {
|
|
3465
|
+
payment_needed_header: paymentNeededHeader,
|
|
3466
|
+
out_trade_no: outTradeNo,
|
|
3467
|
+
pay_before: payBefore,
|
|
3468
|
+
service_id: signedFields.service_id
|
|
3469
|
+
}
|
|
3470
|
+
};
|
|
3471
|
+
return { x402Accepts, paymentNeededHeader };
|
|
3472
|
+
}
|
|
3473
|
+
/**
|
|
3474
|
+
* Verify a `Payment-Proof` by calling
|
|
3475
|
+
* `alipay.aipay.agent.payment.verify` against the Alipay Open API.
|
|
3476
|
+
*
|
|
3477
|
+
* The proof is conveyed via `paymentPayload.payload`, which may be:
|
|
3478
|
+
* - a raw Base64URL string (the `Payment-Proof` header value), or
|
|
3479
|
+
* - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
|
|
3480
|
+
*
|
|
3481
|
+
* All failure modes (malformed payload, network errors, Alipay
|
|
3482
|
+
* `code != 10000`) return `{ valid: false, error }`. No exception
|
|
3483
|
+
* escapes, regardless of client-supplied input.
|
|
3484
|
+
*/
|
|
3485
|
+
async verify(paymentPayload, _requirements) {
|
|
3486
|
+
try {
|
|
3487
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
3488
|
+
const decoded = decodeProof(proofHeader);
|
|
3489
|
+
const response = await alipayOpenApiCall(
|
|
3490
|
+
"alipay.aipay.agent.payment.verify",
|
|
3491
|
+
{
|
|
3492
|
+
payment_proof: decoded.protocol.payment_proof,
|
|
3493
|
+
trade_no: decoded.protocol.trade_no,
|
|
3494
|
+
client_session: decoded.method.client_session
|
|
3495
|
+
},
|
|
3496
|
+
this.getOpenApiConfig()
|
|
3497
|
+
);
|
|
3498
|
+
if (response.code !== "10000") {
|
|
3499
|
+
return {
|
|
3500
|
+
valid: false,
|
|
3501
|
+
error: `alipay verify ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
3502
|
+
details: {
|
|
3503
|
+
code: response.code,
|
|
3504
|
+
sub_code: response.sub_code,
|
|
3505
|
+
sub_msg: response.sub_msg
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
return {
|
|
3510
|
+
valid: true,
|
|
3511
|
+
details: {
|
|
3512
|
+
trade_no: response.trade_no ?? decoded.protocol.trade_no,
|
|
3513
|
+
amount: response.amount,
|
|
3514
|
+
out_trade_no: response.out_trade_no,
|
|
3515
|
+
resource_id: response.resource_id,
|
|
3516
|
+
active: response.active
|
|
3517
|
+
}
|
|
3518
|
+
};
|
|
3519
|
+
} catch (e) {
|
|
3520
|
+
return {
|
|
3521
|
+
valid: false,
|
|
3522
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
/**
|
|
3527
|
+
* Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
|
|
3528
|
+
* service resource has been returned to the buyer.
|
|
3529
|
+
*
|
|
3530
|
+
* Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
|
|
3531
|
+
* "履约确认失败"), this is **fire-and-forget**: the caller (registry /
|
|
3532
|
+
* server) is expected to log fulfillment failures but NOT roll back
|
|
3533
|
+
* the already-delivered resource.
|
|
3534
|
+
*
|
|
3535
|
+
* Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
|
|
3536
|
+
* does the same; the redundant Base64URL decode is negligible).
|
|
3537
|
+
*/
|
|
3538
|
+
async settle(paymentPayload, _requirements) {
|
|
3539
|
+
try {
|
|
3540
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
3541
|
+
const decoded = decodeProof(proofHeader);
|
|
3542
|
+
const tradeNo = decoded.protocol.trade_no;
|
|
3543
|
+
const response = await alipayOpenApiCall(
|
|
3544
|
+
"alipay.aipay.agent.fulfillment.confirm",
|
|
3545
|
+
{ trade_no: tradeNo },
|
|
3546
|
+
this.getOpenApiConfig()
|
|
3547
|
+
);
|
|
3548
|
+
if (response.code !== "10000") {
|
|
3549
|
+
return {
|
|
3550
|
+
success: false,
|
|
3551
|
+
transaction: tradeNo,
|
|
3552
|
+
error: `alipay fulfillment ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
3553
|
+
status: "fulfillment_failed"
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
return {
|
|
3557
|
+
success: true,
|
|
3558
|
+
transaction: tradeNo,
|
|
3559
|
+
status: "fulfilled"
|
|
3560
|
+
};
|
|
3561
|
+
} catch (e) {
|
|
3562
|
+
return {
|
|
3563
|
+
success: false,
|
|
3564
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
/**
|
|
3569
|
+
* Validate that the configured RSA keys parse and that the Open API
|
|
3570
|
+
* gateway is reachable. Does NOT make a real business API call (would
|
|
3571
|
+
* burn quota and could appear as a legitimate verify attempt in logs).
|
|
3572
|
+
*/
|
|
3573
|
+
async healthCheck() {
|
|
3574
|
+
const start = Date.now();
|
|
3575
|
+
try {
|
|
3576
|
+
import_node_crypto3.default.createPrivateKey(this.config.private_key_pem);
|
|
3577
|
+
} catch (e) {
|
|
3578
|
+
return {
|
|
3579
|
+
healthy: false,
|
|
3580
|
+
error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
try {
|
|
3584
|
+
import_node_crypto3.default.createPublicKey(this.config.alipay_public_key_pem);
|
|
3585
|
+
} catch (e) {
|
|
3586
|
+
return {
|
|
3587
|
+
healthy: false,
|
|
3588
|
+
error: `alipay_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
3589
|
+
};
|
|
3590
|
+
}
|
|
3591
|
+
const gatewayUrl = this.config.gateway_url ?? ALIPAY_GATEWAY_PROD;
|
|
3592
|
+
const controller = new AbortController();
|
|
3593
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
3594
|
+
const response = await fetch(gatewayUrl, {
|
|
3595
|
+
method: "HEAD",
|
|
3596
|
+
signal: controller.signal
|
|
3597
|
+
}).catch(() => null);
|
|
3598
|
+
clearTimeout(timeout);
|
|
3599
|
+
const latencyMs = Date.now() - start;
|
|
3600
|
+
if (!response) {
|
|
3601
|
+
return { healthy: false, error: `gateway unreachable: ${gatewayUrl}`, latencyMs };
|
|
3602
|
+
}
|
|
3603
|
+
return { healthy: true, latencyMs };
|
|
3604
|
+
}
|
|
3605
|
+
/** Bundle the facilitator config into the shape openapi.ts wants. */
|
|
3606
|
+
getOpenApiConfig() {
|
|
3607
|
+
return {
|
|
3608
|
+
gateway_url: this.config.gateway_url ?? ALIPAY_GATEWAY_PROD,
|
|
3609
|
+
app_id: this.config.app_id,
|
|
3610
|
+
private_key_pem: this.config.private_key_pem,
|
|
3611
|
+
alipay_public_key_pem: this.config.alipay_public_key_pem,
|
|
3612
|
+
sign_type: this.config.sign_type
|
|
3613
|
+
};
|
|
3614
|
+
}
|
|
3615
|
+
};
|
|
3616
|
+
function generateOutTradeNo() {
|
|
3617
|
+
return "VID" + import_node_crypto3.default.randomBytes(22).toString("base64url").slice(0, 29);
|
|
3618
|
+
}
|
|
3619
|
+
function formatPayBefore(now) {
|
|
3620
|
+
const expiry = new Date(now.getTime() + ALIPAY_PAY_BEFORE_MS);
|
|
3621
|
+
return expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
3622
|
+
}
|
|
3623
|
+
function extractProofHeader(payload) {
|
|
3624
|
+
if (typeof payload === "string") {
|
|
3625
|
+
if (payload.length === 0) {
|
|
3626
|
+
throw new Error("alipay Payment-Proof is empty");
|
|
3627
|
+
}
|
|
3628
|
+
return payload;
|
|
3629
|
+
}
|
|
3630
|
+
if (payload !== null && typeof payload === "object") {
|
|
3631
|
+
const obj = payload;
|
|
3632
|
+
const candidate = obj.paymentProof ?? obj.proofHeader;
|
|
3633
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
3634
|
+
return candidate;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
throw new Error(
|
|
3638
|
+
"alipay payment payload must be a Base64URL string or {paymentProof: string} / {proofHeader: string}"
|
|
3639
|
+
);
|
|
3640
|
+
}
|
|
3641
|
+
function decodeProof(proofHeader) {
|
|
3642
|
+
let parsed;
|
|
3643
|
+
try {
|
|
3644
|
+
parsed = JSON.parse(decodeBase64UrlWithPadFix(proofHeader));
|
|
3645
|
+
} catch (e) {
|
|
3646
|
+
throw new Error(
|
|
3647
|
+
`failed to decode Payment-Proof: ${e instanceof Error ? e.message : String(e)}`
|
|
3648
|
+
);
|
|
3649
|
+
}
|
|
3650
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
3651
|
+
throw new Error("decoded Payment-Proof is not an object");
|
|
3652
|
+
}
|
|
3653
|
+
const obj = parsed;
|
|
3654
|
+
const protocol = obj.protocol;
|
|
3655
|
+
const method = obj.method;
|
|
3656
|
+
if (!protocol || typeof protocol.payment_proof !== "string") {
|
|
3657
|
+
throw new Error("decoded Payment-Proof missing protocol.payment_proof");
|
|
3658
|
+
}
|
|
3659
|
+
if (typeof protocol.trade_no !== "string") {
|
|
3660
|
+
throw new Error("decoded Payment-Proof missing protocol.trade_no");
|
|
3661
|
+
}
|
|
3662
|
+
if (!method || typeof method.client_session !== "string") {
|
|
3663
|
+
throw new Error("decoded Payment-Proof missing method.client_session");
|
|
3664
|
+
}
|
|
3665
|
+
return parsed;
|
|
3666
|
+
}
|
|
3667
|
+
|
|
2214
3668
|
// src/facilitators/registry.ts
|
|
2215
3669
|
init_cjs_shims();
|
|
2216
|
-
var
|
|
3670
|
+
var import_web36 = require("@solana/web3.js");
|
|
2217
3671
|
var import_bs582 = __toESM(require("bs58"));
|
|
2218
3672
|
var FacilitatorRegistry = class {
|
|
2219
3673
|
factories = /* @__PURE__ */ new Map();
|
|
@@ -2229,13 +3683,14 @@ var FacilitatorRegistry = class {
|
|
|
2229
3683
|
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
2230
3684
|
if (feePayerKey) {
|
|
2231
3685
|
try {
|
|
2232
|
-
feePayerKeypair =
|
|
3686
|
+
feePayerKeypair = import_web36.Keypair.fromSecretKey(import_bs582.default.decode(feePayerKey));
|
|
2233
3687
|
} catch (e) {
|
|
2234
3688
|
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
2235
3689
|
}
|
|
2236
3690
|
}
|
|
2237
3691
|
return new SolanaFacilitator({ feePayerKeypair });
|
|
2238
3692
|
});
|
|
3693
|
+
this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
|
|
2239
3694
|
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
2240
3695
|
}
|
|
2241
3696
|
/**
|
|
@@ -2463,6 +3918,11 @@ var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
|
2463
3918
|
var MPP_AUTH_HEADER = "authorization";
|
|
2464
3919
|
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
2465
3920
|
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
3921
|
+
var ALIPAY_PAYMENT_NEEDED_HEADER = "payment-needed";
|
|
3922
|
+
var ALIPAY_PAYMENT_PROOF_HEADER = "payment-proof";
|
|
3923
|
+
function headerSafe(value) {
|
|
3924
|
+
return String(value ?? "").replace(/[^\x20-\x7E]/g, (ch) => encodeURIComponent(ch)).replace(/"/g, "%22");
|
|
3925
|
+
}
|
|
2466
3926
|
var TOKEN_ADDRESSES = {
|
|
2467
3927
|
"eip155:8453": {
|
|
2468
3928
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -2504,7 +3964,7 @@ var TOKEN_ADDRESSES = {
|
|
|
2504
3964
|
// Devnet USDC
|
|
2505
3965
|
}
|
|
2506
3966
|
};
|
|
2507
|
-
var
|
|
3967
|
+
var CHAIN_TO_NETWORK2 = {
|
|
2508
3968
|
"base": "eip155:8453",
|
|
2509
3969
|
"base_sepolia": "eip155:84532",
|
|
2510
3970
|
"polygon": "eip155:137",
|
|
@@ -2535,9 +3995,13 @@ var TOKEN_DOMAINS = {
|
|
|
2535
3995
|
USDT: { name: "(PoS) Tether USD", version: "2" }
|
|
2536
3996
|
},
|
|
2537
3997
|
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
3998
|
+
// Domain names verified against on-chain DOMAIN_SEPARATOR values on 2026-04-21.
|
|
3999
|
+
// See docs/TEMPO-WEB-SUPPORT.md Section 2 and test/server/tempo-domain.test.ts.
|
|
4000
|
+
// All 4 Tempo TIP-20 tokens (pathUSD / AlphaUSD / BetaUSD / ThetaUSD) use
|
|
4001
|
+
// the token symbol with first letter capitalized + version "1".
|
|
2538
4002
|
"eip155:42431": {
|
|
2539
|
-
USDC: { name: "
|
|
2540
|
-
USDT: { name: "
|
|
4003
|
+
USDC: { name: "PathUSD", version: "1" },
|
|
4004
|
+
USDT: { name: "AlphaUSD", version: "1" }
|
|
2541
4005
|
},
|
|
2542
4006
|
// BNB Smart Chain mainnet
|
|
2543
4007
|
"eip155:56": {
|
|
@@ -2594,6 +4058,8 @@ var MoltsPayServer = class {
|
|
|
2594
4058
|
registry;
|
|
2595
4059
|
networkId;
|
|
2596
4060
|
useMainnet;
|
|
4061
|
+
/** Alipay AI 收 facilitator instance, set when `provider.alipay` is configured (2.0.0). */
|
|
4062
|
+
alipayFacilitator = null;
|
|
2597
4063
|
constructor(servicesPath, options = {}) {
|
|
2598
4064
|
loadEnvFile2();
|
|
2599
4065
|
const content = (0, import_fs4.readFileSync)(servicesPath, "utf-8");
|
|
@@ -2615,7 +4081,38 @@ var MoltsPayServer = class {
|
|
|
2615
4081
|
cdp: { useMainnet: this.useMainnet }
|
|
2616
4082
|
}
|
|
2617
4083
|
};
|
|
4084
|
+
const providerAlipay = this.manifest.provider.alipay;
|
|
4085
|
+
if (providerAlipay) {
|
|
4086
|
+
try {
|
|
4087
|
+
const baseDir = path2.dirname(servicesPath);
|
|
4088
|
+
const resolvePem = (p, kind) => toPem((0, import_fs4.readFileSync)(path2.isAbsolute(p) ? p : path2.resolve(baseDir, p), "utf-8"), kind);
|
|
4089
|
+
const alipayFacilitatorConfig = {
|
|
4090
|
+
seller_id: providerAlipay.seller_id,
|
|
4091
|
+
app_id: providerAlipay.app_id,
|
|
4092
|
+
seller_name: providerAlipay.seller_name,
|
|
4093
|
+
service_id_default: providerAlipay.service_id_default,
|
|
4094
|
+
private_key_pem: resolvePem(providerAlipay.private_key_path, "PRIVATE"),
|
|
4095
|
+
alipay_public_key_pem: resolvePem(providerAlipay.alipay_public_key_path, "PUBLIC"),
|
|
4096
|
+
gateway_url: providerAlipay.gateway_url,
|
|
4097
|
+
sign_type: providerAlipay.sign_type
|
|
4098
|
+
};
|
|
4099
|
+
facilitatorConfig.config = {
|
|
4100
|
+
...facilitatorConfig.config,
|
|
4101
|
+
alipay: alipayFacilitatorConfig
|
|
4102
|
+
};
|
|
4103
|
+
facilitatorConfig.fallback = facilitatorConfig.fallback || [];
|
|
4104
|
+
if (facilitatorConfig.primary !== "alipay" && !facilitatorConfig.fallback.includes("alipay")) {
|
|
4105
|
+
facilitatorConfig.fallback.push("alipay");
|
|
4106
|
+
}
|
|
4107
|
+
} catch (err) {
|
|
4108
|
+
throw new Error(`[MoltsPay] Alipay rail configured but key load failed: ${err.message}`);
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
2618
4111
|
this.registry = new FacilitatorRegistry(facilitatorConfig);
|
|
4112
|
+
if (providerAlipay) {
|
|
4113
|
+
this.alipayFacilitator = this.registry.get("alipay");
|
|
4114
|
+
console.log(`[MoltsPay] Alipay AI \u6536 rail enabled (seller ${providerAlipay.seller_id})`);
|
|
4115
|
+
}
|
|
2619
4116
|
const primaryFacilitator = this.registry.get(facilitatorConfig.primary);
|
|
2620
4117
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
2621
4118
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
@@ -2660,14 +4157,14 @@ var MoltsPayServer = class {
|
|
|
2660
4157
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
2661
4158
|
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
2662
4159
|
return {
|
|
2663
|
-
network:
|
|
4160
|
+
network: CHAIN_TO_NETWORK2[chainName] || "eip155:8453",
|
|
2664
4161
|
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
2665
4162
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
2666
4163
|
};
|
|
2667
4164
|
});
|
|
2668
4165
|
}
|
|
2669
4166
|
const chain = provider.chain || "base";
|
|
2670
|
-
const network =
|
|
4167
|
+
const network = CHAIN_TO_NETWORK2[chain] || this.networkId;
|
|
2671
4168
|
return [{
|
|
2672
4169
|
network,
|
|
2673
4170
|
wallet: getWalletForChain(chain),
|
|
@@ -2705,14 +4202,63 @@ var MoltsPayServer = class {
|
|
|
2705
4202
|
console.log(` GET /health - Health check (incl. facilitators)`);
|
|
2706
4203
|
});
|
|
2707
4204
|
}
|
|
4205
|
+
/**
|
|
4206
|
+
* Apply CORS response headers according to the `cors` option.
|
|
4207
|
+
*
|
|
4208
|
+
* Default (`cors` unset or `true`): `Access-Control-Allow-Origin: *`. Matches 1.5.x behavior
|
|
4209
|
+
* and works for every browser client whose origin does not need to send cookies.
|
|
4210
|
+
*
|
|
4211
|
+
* `cors: false`: emit no CORS headers. Same-origin only.
|
|
4212
|
+
* `cors: string[]`: origin allowlist — echo the origin back iff it matches.
|
|
4213
|
+
* `cors: CorsOptions`: full control (allowlist + credentials + maxAge).
|
|
4214
|
+
*
|
|
4215
|
+
* The required-for-Web response headers are always exposed when CORS is active:
|
|
4216
|
+
* `X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt`.
|
|
4217
|
+
*/
|
|
4218
|
+
applyCorsHeaders(req, res) {
|
|
4219
|
+
const cors = this.options.cors;
|
|
4220
|
+
if (cors === false) {
|
|
4221
|
+
return;
|
|
4222
|
+
}
|
|
4223
|
+
const requestOrigin = req.headers.origin ?? "*";
|
|
4224
|
+
if (cors === void 0 || cors === true) {
|
|
4225
|
+
this.writeCorsHeaders(res, "*");
|
|
4226
|
+
return;
|
|
4227
|
+
}
|
|
4228
|
+
if (Array.isArray(cors)) {
|
|
4229
|
+
if (cors.includes(requestOrigin)) {
|
|
4230
|
+
this.writeCorsHeaders(res, requestOrigin);
|
|
4231
|
+
res.setHeader("Vary", "Origin");
|
|
4232
|
+
}
|
|
4233
|
+
return;
|
|
4234
|
+
}
|
|
4235
|
+
const opt = cors;
|
|
4236
|
+
const isAllowed = typeof opt.origins === "function" ? opt.origins(requestOrigin) : opt.origins.includes(requestOrigin);
|
|
4237
|
+
if (!isAllowed) {
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4240
|
+
this.writeCorsHeaders(res, requestOrigin);
|
|
4241
|
+
res.setHeader("Vary", "Origin");
|
|
4242
|
+
if (opt.credentials) {
|
|
4243
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
4244
|
+
}
|
|
4245
|
+
const maxAge = opt.maxAge ?? 600;
|
|
4246
|
+
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
4247
|
+
}
|
|
4248
|
+
writeCorsHeaders(res, origin) {
|
|
4249
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
4250
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
4251
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization, Payment-Proof");
|
|
4252
|
+
res.setHeader(
|
|
4253
|
+
"Access-Control-Expose-Headers",
|
|
4254
|
+
"X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt, Payment-Needed"
|
|
4255
|
+
);
|
|
4256
|
+
}
|
|
2708
4257
|
/**
|
|
2709
4258
|
* Handle incoming request
|
|
2710
4259
|
*/
|
|
2711
4260
|
async handleRequest(req, res) {
|
|
2712
|
-
|
|
2713
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2714
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
2715
|
-
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
|
|
4261
|
+
this.applyCorsHeaders(req, res);
|
|
2716
4262
|
if (req.method === "OPTIONS") {
|
|
2717
4263
|
res.writeHead(204);
|
|
2718
4264
|
res.end();
|
|
@@ -2732,7 +4278,8 @@ var MoltsPayServer = class {
|
|
|
2732
4278
|
if (url.pathname === "/execute" && req.method === "POST") {
|
|
2733
4279
|
const body = await this.readBody(req);
|
|
2734
4280
|
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
2735
|
-
|
|
4281
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
4282
|
+
return await this.handleExecute(body, paymentHeader, res, proofHeader);
|
|
2736
4283
|
}
|
|
2737
4284
|
if (url.pathname === "/proxy" && req.method === "POST") {
|
|
2738
4285
|
const clientIP = req.headers["x-real-ip"] || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "";
|
|
@@ -2750,7 +4297,8 @@ var MoltsPayServer = class {
|
|
|
2750
4297
|
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
2751
4298
|
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
2752
4299
|
const x402Header = req.headers[PAYMENT_HEADER2];
|
|
2753
|
-
|
|
4300
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
4301
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader);
|
|
2754
4302
|
}
|
|
2755
4303
|
this.sendJson(res, 404, { error: "Not found" });
|
|
2756
4304
|
} catch (err) {
|
|
@@ -2847,7 +4395,7 @@ var MoltsPayServer = class {
|
|
|
2847
4395
|
/**
|
|
2848
4396
|
* POST /execute - Execute service with x402 payment
|
|
2849
4397
|
*/
|
|
2850
|
-
async handleExecute(body, paymentHeader, res) {
|
|
4398
|
+
async handleExecute(body, paymentHeader, res, proofHeader) {
|
|
2851
4399
|
const { service, params } = body;
|
|
2852
4400
|
if (!service) {
|
|
2853
4401
|
return this.sendJson(res, 400, { error: "Missing service" });
|
|
@@ -2861,6 +4409,15 @@ var MoltsPayServer = class {
|
|
|
2861
4409
|
return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
|
|
2862
4410
|
}
|
|
2863
4411
|
}
|
|
4412
|
+
if (proofHeader) {
|
|
4413
|
+
const alipayPayment = {
|
|
4414
|
+
x402Version: X402_VERSION3,
|
|
4415
|
+
scheme: ALIPAY_SCHEME,
|
|
4416
|
+
network: ALIPAY_NETWORK,
|
|
4417
|
+
payload: proofHeader
|
|
4418
|
+
};
|
|
4419
|
+
return this.handleAlipayExecute(skill, params || {}, alipayPayment, res);
|
|
4420
|
+
}
|
|
2864
4421
|
if (!paymentHeader) {
|
|
2865
4422
|
return this.sendPaymentRequired(skill.config, res);
|
|
2866
4423
|
}
|
|
@@ -2871,6 +4428,11 @@ var MoltsPayServer = class {
|
|
|
2871
4428
|
} catch {
|
|
2872
4429
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
2873
4430
|
}
|
|
4431
|
+
const payScheme = payment.accepted?.scheme || payment.scheme;
|
|
4432
|
+
const payNetwork = payment.accepted?.network || payment.network;
|
|
4433
|
+
if (payScheme === ALIPAY_SCHEME || (payNetwork ? isAlipayChainId(payNetwork) : false)) {
|
|
4434
|
+
return this.handleAlipayExecute(skill, params || {}, payment, res);
|
|
4435
|
+
}
|
|
2874
4436
|
const validation = this.validatePayment(payment, skill.config);
|
|
2875
4437
|
if (!validation.valid) {
|
|
2876
4438
|
return this.sendJson(res, 402, { error: validation.error });
|
|
@@ -2935,6 +4497,14 @@ var MoltsPayServer = class {
|
|
|
2935
4497
|
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2936
4498
|
} catch (err) {
|
|
2937
4499
|
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
4500
|
+
settlement = { success: false, error: err.message, facilitator: "none" };
|
|
4501
|
+
}
|
|
4502
|
+
if (!settlement?.success) {
|
|
4503
|
+
return this.sendJson(res, 402, {
|
|
4504
|
+
error: "Payment settlement failed",
|
|
4505
|
+
message: settlement?.error || "Settlement returned no success state",
|
|
4506
|
+
facilitator: settlement?.facilitator
|
|
4507
|
+
});
|
|
2938
4508
|
}
|
|
2939
4509
|
}
|
|
2940
4510
|
const responseHeaders = {};
|
|
@@ -2955,13 +4525,111 @@ var MoltsPayServer = class {
|
|
|
2955
4525
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
2956
4526
|
}, responseHeaders);
|
|
2957
4527
|
}
|
|
4528
|
+
/**
|
|
4529
|
+
* Execute a service paid via the Alipay AI 收 fiat rail (2.0.0).
|
|
4530
|
+
*
|
|
4531
|
+
* Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
|
|
4532
|
+
* validation. Verify hits the Alipay Open API (`payment.verify`). Settlement
|
|
4533
|
+
* (`fulfillment.confirm`) is FIRE-AND-FORGET per ALIPAY-INTEGRATION-DESIGN
|
|
4534
|
+
* §5.1: a confirm failure is logged but does NOT fail the already-delivered
|
|
4535
|
+
* response (the buyer's payment proof was already verified).
|
|
4536
|
+
*/
|
|
4537
|
+
async handleAlipayExecute(skill, params, payment, res) {
|
|
4538
|
+
if (!this.alipayFacilitator) {
|
|
4539
|
+
return this.sendJson(res, 402, { error: "Alipay rail not configured on this server" });
|
|
4540
|
+
}
|
|
4541
|
+
const requirements = {
|
|
4542
|
+
scheme: ALIPAY_SCHEME,
|
|
4543
|
+
network: ALIPAY_NETWORK,
|
|
4544
|
+
asset: "CNY",
|
|
4545
|
+
amount: skill.config.alipay?.price_cny || "0",
|
|
4546
|
+
payTo: this.manifest.provider.alipay?.seller_id || "",
|
|
4547
|
+
maxTimeoutSeconds: 1800
|
|
4548
|
+
};
|
|
4549
|
+
console.log(`[MoltsPay] Verifying Alipay payment...`);
|
|
4550
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
4551
|
+
if (!verifyResult.valid) {
|
|
4552
|
+
return this.sendJson(res, 402, {
|
|
4553
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
4554
|
+
facilitator: verifyResult.facilitator
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4557
|
+
console.log(`[MoltsPay] Alipay payment verified by ${verifyResult.facilitator}`);
|
|
4558
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
4559
|
+
console.log(`[MoltsPay] Executing skill: ${skill.id} (timeout: ${timeoutSeconds}s)`);
|
|
4560
|
+
let result;
|
|
4561
|
+
try {
|
|
4562
|
+
result = await Promise.race([
|
|
4563
|
+
skill.handler(params),
|
|
4564
|
+
new Promise(
|
|
4565
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
4566
|
+
)
|
|
4567
|
+
]);
|
|
4568
|
+
} catch (err) {
|
|
4569
|
+
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
4570
|
+
return this.sendJson(res, 500, {
|
|
4571
|
+
error: "Service execution failed",
|
|
4572
|
+
message: err.message
|
|
4573
|
+
});
|
|
4574
|
+
}
|
|
4575
|
+
let settlement;
|
|
4576
|
+
try {
|
|
4577
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
4578
|
+
if (settlement.success) {
|
|
4579
|
+
console.log(`[MoltsPay] Alipay fulfillment confirmed: ${settlement.transaction}`);
|
|
4580
|
+
} else {
|
|
4581
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm failed (non-fatal): ${settlement.error}`);
|
|
4582
|
+
}
|
|
4583
|
+
} catch (err) {
|
|
4584
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm threw (non-fatal): ${err.message}`);
|
|
4585
|
+
settlement = { success: false, error: err.message, facilitator: "alipay" };
|
|
4586
|
+
}
|
|
4587
|
+
const responseHeaders = {};
|
|
4588
|
+
if (settlement.success) {
|
|
4589
|
+
responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(JSON.stringify({
|
|
4590
|
+
success: true,
|
|
4591
|
+
transaction: settlement.transaction,
|
|
4592
|
+
network: ALIPAY_NETWORK,
|
|
4593
|
+
facilitator: settlement.facilitator
|
|
4594
|
+
})).toString("base64");
|
|
4595
|
+
}
|
|
4596
|
+
this.sendJson(res, 200, {
|
|
4597
|
+
success: true,
|
|
4598
|
+
result,
|
|
4599
|
+
payment: settlement.success ? { transaction: settlement.transaction, status: "fulfilled", facilitator: settlement.facilitator } : { status: "delivered_unconfirmed", error: settlement.error }
|
|
4600
|
+
}, responseHeaders);
|
|
4601
|
+
}
|
|
4602
|
+
/**
|
|
4603
|
+
* Build the Alipay 402 challenge for a service, or null when the alipay rail
|
|
4604
|
+
* isn't configured for this server or this service. Returns the x402
|
|
4605
|
+
* `accepts[]` entry plus the Base64URL `Payment-Needed` header value so the
|
|
4606
|
+
* 402 responders can dual-emit both the x402 and legacy alipay-bot formats.
|
|
4607
|
+
*/
|
|
4608
|
+
async buildAlipayChallenge(config) {
|
|
4609
|
+
if (!this.alipayFacilitator || !config.alipay) return null;
|
|
4610
|
+
try {
|
|
4611
|
+
const req = await this.alipayFacilitator.createPaymentRequirements({
|
|
4612
|
+
serviceId: config.alipay.service_id || this.manifest.provider.alipay.service_id_default,
|
|
4613
|
+
priceCny: config.alipay.price_cny,
|
|
4614
|
+
goodsName: config.alipay.goods_name,
|
|
4615
|
+
resourceId: `/execute?service=${config.id}`
|
|
4616
|
+
});
|
|
4617
|
+
return { accepts: req.x402Accepts, paymentNeededHeader: req.paymentNeededHeader };
|
|
4618
|
+
} catch (err) {
|
|
4619
|
+
console.error(`[MoltsPay] Alipay challenge build failed for ${config.id}: ${err.message}`);
|
|
4620
|
+
return null;
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
2958
4623
|
/**
|
|
2959
4624
|
* Handle MPP (Machine Payments Protocol) request
|
|
2960
4625
|
* Supports both x402 and MPP protocols on service endpoints
|
|
2961
4626
|
*/
|
|
2962
|
-
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
4627
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader) {
|
|
2963
4628
|
const config = skill.config;
|
|
2964
4629
|
const params = body || {};
|
|
4630
|
+
if (proofHeader) {
|
|
4631
|
+
return await this.handleExecute({ service: config.id, params }, void 0, res, proofHeader);
|
|
4632
|
+
}
|
|
2965
4633
|
if (x402Header) {
|
|
2966
4634
|
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
2967
4635
|
}
|
|
@@ -3067,7 +4735,7 @@ var MoltsPayServer = class {
|
|
|
3067
4735
|
/**
|
|
3068
4736
|
* Return 402 with both x402 and MPP payment requirements
|
|
3069
4737
|
*/
|
|
3070
|
-
sendMPPPaymentRequired(config, res) {
|
|
4738
|
+
async sendMPPPaymentRequired(config, res) {
|
|
3071
4739
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
3072
4740
|
const providerChains = this.getProviderChains();
|
|
3073
4741
|
const accepts = [];
|
|
@@ -3078,6 +4746,10 @@ var MoltsPayServer = class {
|
|
|
3078
4746
|
}
|
|
3079
4747
|
}
|
|
3080
4748
|
}
|
|
4749
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
4750
|
+
if (alipayChallenge) {
|
|
4751
|
+
accepts.push(alipayChallenge.accepts);
|
|
4752
|
+
}
|
|
3081
4753
|
const x402PaymentRequired = {
|
|
3082
4754
|
x402Version: X402_VERSION3,
|
|
3083
4755
|
accepts,
|
|
@@ -3105,7 +4777,7 @@ var MoltsPayServer = class {
|
|
|
3105
4777
|
};
|
|
3106
4778
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3107
4779
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3108
|
-
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
4780
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${headerSafe(this.manifest.provider.name)}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
3109
4781
|
}
|
|
3110
4782
|
const headers = {
|
|
3111
4783
|
"Content-Type": "application/problem+json",
|
|
@@ -3114,6 +4786,9 @@ var MoltsPayServer = class {
|
|
|
3114
4786
|
if (mppWwwAuth) {
|
|
3115
4787
|
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
3116
4788
|
}
|
|
4789
|
+
if (alipayChallenge) {
|
|
4790
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
4791
|
+
}
|
|
3117
4792
|
res.writeHead(402, headers);
|
|
3118
4793
|
res.end(JSON.stringify({
|
|
3119
4794
|
type: "https://paymentauth.org/problems/payment-required",
|
|
@@ -3140,7 +4815,7 @@ var MoltsPayServer = class {
|
|
|
3140
4815
|
* Return 402 with x402 payment requirements (v2 format)
|
|
3141
4816
|
* Includes requirements for all chains and all accepted currencies
|
|
3142
4817
|
*/
|
|
3143
|
-
sendPaymentRequired(config, res) {
|
|
4818
|
+
async sendPaymentRequired(config, res) {
|
|
3144
4819
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
3145
4820
|
const providerChains = this.getProviderChains();
|
|
3146
4821
|
const accepts = [];
|
|
@@ -3151,6 +4826,10 @@ var MoltsPayServer = class {
|
|
|
3151
4826
|
}
|
|
3152
4827
|
}
|
|
3153
4828
|
}
|
|
4829
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
4830
|
+
if (alipayChallenge) {
|
|
4831
|
+
accepts.push(alipayChallenge.accepts);
|
|
4832
|
+
}
|
|
3154
4833
|
const acceptedChains = providerChains.map((c) => {
|
|
3155
4834
|
if (c.network === "eip155:8453") return "base";
|
|
3156
4835
|
if (c.network === "eip155:137") return "polygon";
|
|
@@ -3168,10 +4847,14 @@ var MoltsPayServer = class {
|
|
|
3168
4847
|
}
|
|
3169
4848
|
};
|
|
3170
4849
|
const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
3171
|
-
|
|
4850
|
+
const headers = {
|
|
3172
4851
|
"Content-Type": "application/json",
|
|
3173
4852
|
[PAYMENT_REQUIRED_HEADER2]: encoded
|
|
3174
|
-
}
|
|
4853
|
+
};
|
|
4854
|
+
if (alipayChallenge) {
|
|
4855
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
4856
|
+
}
|
|
4857
|
+
res.writeHead(402, headers);
|
|
3175
4858
|
res.end(JSON.stringify({
|
|
3176
4859
|
error: "Payment required",
|
|
3177
4860
|
message: `Service requires $${config.price} ${config.currency}`,
|
|
@@ -3189,7 +4872,7 @@ var MoltsPayServer = class {
|
|
|
3189
4872
|
}
|
|
3190
4873
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
3191
4874
|
const network = payment.accepted?.network || payment.network || this.networkId;
|
|
3192
|
-
if (scheme !== "exact") {
|
|
4875
|
+
if (scheme !== "exact" && scheme !== "permit") {
|
|
3193
4876
|
return { valid: false, error: `Unsupported scheme: ${scheme}` };
|
|
3194
4877
|
}
|
|
3195
4878
|
if (!this.isNetworkAccepted(network)) {
|
|
@@ -3211,8 +4894,10 @@ var MoltsPayServer = class {
|
|
|
3211
4894
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
3212
4895
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
3213
4896
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
4897
|
+
const isTempo = selectedNetwork === "eip155:42431";
|
|
4898
|
+
const scheme = isTempo ? "permit" : "exact";
|
|
3214
4899
|
const requirements = {
|
|
3215
|
-
scheme
|
|
4900
|
+
scheme,
|
|
3216
4901
|
network: selectedNetwork,
|
|
3217
4902
|
asset: tokenAddress,
|
|
3218
4903
|
amount: amountInUnits,
|
|
@@ -3240,6 +4925,16 @@ var MoltsPayServer = class {
|
|
|
3240
4925
|
};
|
|
3241
4926
|
}
|
|
3242
4927
|
}
|
|
4928
|
+
if (isTempo) {
|
|
4929
|
+
const tempoFacilitator = this.registry.get("tempo");
|
|
4930
|
+
const tempoSpender = tempoFacilitator?.getSpenderAddress?.();
|
|
4931
|
+
if (tempoSpender) {
|
|
4932
|
+
requirements.extra = {
|
|
4933
|
+
...requirements.extra || {},
|
|
4934
|
+
tempoSpender
|
|
4935
|
+
};
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
3243
4938
|
return requirements;
|
|
3244
4939
|
}
|
|
3245
4940
|
/**
|
|
@@ -3266,12 +4961,12 @@ var MoltsPayServer = class {
|
|
|
3266
4961
|
return accepted.includes(token);
|
|
3267
4962
|
}
|
|
3268
4963
|
async readBody(req) {
|
|
3269
|
-
return new Promise((
|
|
4964
|
+
return new Promise((resolve3, reject) => {
|
|
3270
4965
|
let body = "";
|
|
3271
4966
|
req.on("data", (chunk) => body += chunk);
|
|
3272
4967
|
req.on("end", () => {
|
|
3273
4968
|
try {
|
|
3274
|
-
|
|
4969
|
+
resolve3(body ? JSON.parse(body) : {});
|
|
3275
4970
|
} catch {
|
|
3276
4971
|
reject(new Error("Invalid JSON"));
|
|
3277
4972
|
}
|
|
@@ -3374,10 +5069,10 @@ var MoltsPayServer = class {
|
|
|
3374
5069
|
}
|
|
3375
5070
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
3376
5071
|
const network = payment.accepted?.network || payment.network;
|
|
3377
|
-
if (scheme !== "exact") {
|
|
5072
|
+
if (scheme !== "exact" && scheme !== "permit") {
|
|
3378
5073
|
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
3379
5074
|
}
|
|
3380
|
-
const expectedNetwork = chain ?
|
|
5075
|
+
const expectedNetwork = chain ? CHAIN_TO_NETWORK2[chain] || this.networkId : this.networkId;
|
|
3381
5076
|
if (network !== expectedNetwork) {
|
|
3382
5077
|
return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
|
|
3383
5078
|
}
|
|
@@ -3529,7 +5224,7 @@ var MoltsPayServer = class {
|
|
|
3529
5224
|
};
|
|
3530
5225
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3531
5226
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3532
|
-
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
5227
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
3533
5228
|
res.writeHead(402, {
|
|
3534
5229
|
"Content-Type": "application/problem+json",
|
|
3535
5230
|
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
@@ -3639,7 +5334,7 @@ var MoltsPayServer = class {
|
|
|
3639
5334
|
buildProxyPaymentRequirements(config, wallet, token, chain) {
|
|
3640
5335
|
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
3641
5336
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
3642
|
-
const networkId = chain ?
|
|
5337
|
+
const networkId = chain ? CHAIN_TO_NETWORK2[chain] || this.networkId : this.networkId;
|
|
3643
5338
|
const selectedToken = token && acceptedTokens.includes(token) ? token : acceptedTokens[0];
|
|
3644
5339
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
3645
5340
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
@@ -3699,10 +5394,10 @@ init_cjs_shims();
|
|
|
3699
5394
|
async function printQRCode(url) {
|
|
3700
5395
|
const qrcodeModule = await import("qrcode-terminal");
|
|
3701
5396
|
const qrcode = qrcodeModule.default || qrcodeModule;
|
|
3702
|
-
return new Promise((
|
|
5397
|
+
return new Promise((resolve3) => {
|
|
3703
5398
|
qrcode.generate(url, { small: true }, (qr) => {
|
|
3704
5399
|
console.log(qr);
|
|
3705
|
-
|
|
5400
|
+
resolve3();
|
|
3706
5401
|
});
|
|
3707
5402
|
});
|
|
3708
5403
|
}
|
|
@@ -3714,9 +5409,9 @@ if (!globalThis.crypto) {
|
|
|
3714
5409
|
}
|
|
3715
5410
|
function getVersion() {
|
|
3716
5411
|
const locations = [
|
|
3717
|
-
(0,
|
|
3718
|
-
(0,
|
|
3719
|
-
(0,
|
|
5412
|
+
(0, import_path4.join)(__dirname, "../../package.json"),
|
|
5413
|
+
(0, import_path4.join)(__dirname, "../package.json"),
|
|
5414
|
+
(0, import_path4.join)(process.cwd(), "node_modules/moltspay/package.json")
|
|
3720
5415
|
];
|
|
3721
5416
|
for (const loc of locations) {
|
|
3722
5417
|
try {
|
|
@@ -3737,7 +5432,7 @@ var ERC20_APPROVE_ABI = [
|
|
|
3737
5432
|
];
|
|
3738
5433
|
async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = false) {
|
|
3739
5434
|
const chainConfig = CHAINS[chain];
|
|
3740
|
-
const provider = new
|
|
5435
|
+
const provider = new import_ethers4.ethers.JsonRpcProvider(chainConfig.rpc);
|
|
3741
5436
|
const wallet = client.getWallet();
|
|
3742
5437
|
if (!wallet) {
|
|
3743
5438
|
console.log(" \u274C No wallet found");
|
|
@@ -3746,15 +5441,15 @@ async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = fal
|
|
|
3746
5441
|
const signer = wallet.connect(provider);
|
|
3747
5442
|
console.log(` Spender: ${spenderAddress}`);
|
|
3748
5443
|
let bnbBalance = await provider.getBalance(wallet.address);
|
|
3749
|
-
const minGasRequired =
|
|
5444
|
+
const minGasRequired = import_ethers4.ethers.parseEther("0.0005");
|
|
3750
5445
|
if (bnbBalance < minGasRequired) {
|
|
3751
5446
|
if (sponsorGas && BNB_SPONSOR_KEY) {
|
|
3752
5447
|
console.log(" \u23F3 Sponsoring BNB gas for approvals...");
|
|
3753
5448
|
try {
|
|
3754
|
-
const sponsorWallet = new
|
|
5449
|
+
const sponsorWallet = new import_ethers4.ethers.Wallet(BNB_SPONSOR_KEY, provider);
|
|
3755
5450
|
const tx = await sponsorWallet.sendTransaction({
|
|
3756
5451
|
to: wallet.address,
|
|
3757
|
-
value:
|
|
5452
|
+
value: import_ethers4.ethers.parseEther("0.001")
|
|
3758
5453
|
});
|
|
3759
5454
|
await tx.wait();
|
|
3760
5455
|
console.log(` \u2705 Sponsored 0.001 BNB (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
@@ -3773,7 +5468,7 @@ async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = fal
|
|
|
3773
5468
|
}
|
|
3774
5469
|
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3775
5470
|
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3776
|
-
const tokenContract = new
|
|
5471
|
+
const tokenContract = new import_ethers4.ethers.Contract(tokenConfig.address, ERC20_APPROVE_ABI, signer);
|
|
3777
5472
|
const allowance = await tokenContract.allowance(wallet.address, spenderAddress);
|
|
3778
5473
|
if (allowance > 0n) {
|
|
3779
5474
|
console.log(` \u2705 ${tokenSymbol}: already approved for ${spenderAddress.slice(0, 10)}...`);
|
|
@@ -3781,7 +5476,7 @@ async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = fal
|
|
|
3781
5476
|
}
|
|
3782
5477
|
console.log(` \u23F3 Approving ${tokenSymbol}...`);
|
|
3783
5478
|
try {
|
|
3784
|
-
const tx = await tokenContract.approve(spenderAddress,
|
|
5479
|
+
const tx = await tokenContract.approve(spenderAddress, import_ethers4.ethers.MaxUint256);
|
|
3785
5480
|
await tx.wait();
|
|
3786
5481
|
console.log(` \u2705 ${tokenSymbol}: approved (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3787
5482
|
} catch (err) {
|
|
@@ -3792,10 +5487,10 @@ async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = fal
|
|
|
3792
5487
|
}
|
|
3793
5488
|
async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2) {
|
|
3794
5489
|
const chainConfig = CHAINS[chain];
|
|
3795
|
-
const provider = new
|
|
5490
|
+
const provider = new import_ethers4.ethers.JsonRpcProvider(chainConfig.rpc);
|
|
3796
5491
|
let spenderAddress = null;
|
|
3797
5492
|
try {
|
|
3798
|
-
const walletPath = (0,
|
|
5493
|
+
const walletPath = (0, import_path4.join)(configDir, "wallet.json");
|
|
3799
5494
|
const walletData = JSON.parse((0, import_fs5.readFileSync)(walletPath, "utf-8"));
|
|
3800
5495
|
spenderAddress = walletData.approvals?.[chain] || null;
|
|
3801
5496
|
} catch {
|
|
@@ -3806,15 +5501,15 @@ async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2
|
|
|
3806
5501
|
}
|
|
3807
5502
|
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3808
5503
|
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3809
|
-
const tokenContract = new
|
|
5504
|
+
const tokenContract = new import_ethers4.ethers.Contract(tokenConfig.address, ERC20_APPROVE_ABI, provider);
|
|
3810
5505
|
const allowance = await tokenContract.allowance(address, spenderAddress);
|
|
3811
5506
|
result[tokenSymbol.toLowerCase()] = allowance > 0n;
|
|
3812
5507
|
}
|
|
3813
5508
|
return result;
|
|
3814
5509
|
}
|
|
3815
5510
|
var program = new import_commander.Command();
|
|
3816
|
-
var DEFAULT_CONFIG_DIR2 = (0,
|
|
3817
|
-
var PID_FILE = (0,
|
|
5511
|
+
var DEFAULT_CONFIG_DIR2 = (0, import_path4.join)((0, import_os4.homedir)(), ".moltspay");
|
|
5512
|
+
var PID_FILE = (0, import_path4.join)(DEFAULT_CONFIG_DIR2, "server.pid");
|
|
3818
5513
|
if (!(0, import_fs5.existsSync)(DEFAULT_CONFIG_DIR2)) {
|
|
3819
5514
|
(0, import_fs5.mkdirSync)(DEFAULT_CONFIG_DIR2, { recursive: true });
|
|
3820
5515
|
}
|
|
@@ -3823,10 +5518,10 @@ function prompt(question) {
|
|
|
3823
5518
|
input: process.stdin,
|
|
3824
5519
|
output: process.stdout
|
|
3825
5520
|
});
|
|
3826
|
-
return new Promise((
|
|
5521
|
+
return new Promise((resolve3) => {
|
|
3827
5522
|
rl.question(question, (answer) => {
|
|
3828
5523
|
rl.close();
|
|
3829
|
-
|
|
5524
|
+
resolve3(answer.trim());
|
|
3830
5525
|
});
|
|
3831
5526
|
});
|
|
3832
5527
|
}
|
|
@@ -3854,7 +5549,7 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
3854
5549
|
console.log(`
|
|
3855
5550
|
\u2705 Solana wallet created: ${address}`);
|
|
3856
5551
|
console.log(`
|
|
3857
|
-
\u{1F4C1} Config saved to: ${(0,
|
|
5552
|
+
\u{1F4C1} Config saved to: ${(0, import_path4.join)(options.configDir, "wallet-solana.json")}`);
|
|
3858
5553
|
console.log(`
|
|
3859
5554
|
\u26A0\uFE0F IMPORTANT: Back up your wallet file!`);
|
|
3860
5555
|
console.log(` This file contains your private key!
|
|
@@ -3869,7 +5564,7 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
3869
5564
|
return;
|
|
3870
5565
|
}
|
|
3871
5566
|
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
3872
|
-
if ((0, import_fs5.existsSync)((0,
|
|
5567
|
+
if ((0, import_fs5.existsSync)((0, import_path4.join)(options.configDir, "wallet.json"))) {
|
|
3873
5568
|
console.log('\u26A0\uFE0F EVM wallet already initialized. Use "moltspay config" to update settings.');
|
|
3874
5569
|
console.log(` Config dir: ${options.configDir}`);
|
|
3875
5570
|
return;
|
|
@@ -3895,7 +5590,7 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
3895
5590
|
console.log(`
|
|
3896
5591
|
\u{1F4C1} Config saved to: ${result.configDir}`);
|
|
3897
5592
|
console.log(`
|
|
3898
|
-
\u26A0\uFE0F IMPORTANT: Back up ${(0,
|
|
5593
|
+
\u26A0\uFE0F IMPORTANT: Back up ${(0, import_path4.join)(result.configDir, "wallet.json")}`);
|
|
3899
5594
|
console.log(` This file contains your private key!
|
|
3900
5595
|
`);
|
|
3901
5596
|
if (chain === "bnb" || chain === "bnb_testnet") {
|
|
@@ -4060,7 +5755,7 @@ program.command("approve").description("Approve a spender address for BNB chain
|
|
|
4060
5755
|
\u{1F510} Approving spender for ${chain}...
|
|
4061
5756
|
`);
|
|
4062
5757
|
await setupBNBApprovals(client, chain, options.spender, false);
|
|
4063
|
-
const walletPath = (0,
|
|
5758
|
+
const walletPath = (0, import_path4.join)(options.configDir || DEFAULT_CONFIG_DIR2, "wallet.json");
|
|
4064
5759
|
try {
|
|
4065
5760
|
const walletData = JSON.parse((0, import_fs5.readFileSync)(walletPath, "utf-8"));
|
|
4066
5761
|
walletData.approvals = walletData.approvals || {};
|
|
@@ -4697,17 +6392,17 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
4697
6392
|
const handlers = /* @__PURE__ */ new Map();
|
|
4698
6393
|
let provider = null;
|
|
4699
6394
|
for (const inputPath of allPaths) {
|
|
4700
|
-
const resolvedPath = (0,
|
|
6395
|
+
const resolvedPath = (0, import_path4.resolve)(inputPath);
|
|
4701
6396
|
let manifestPath;
|
|
4702
6397
|
let skillDir;
|
|
4703
6398
|
let isSkillDir = false;
|
|
4704
|
-
if ((0, import_fs5.existsSync)((0,
|
|
4705
|
-
manifestPath = (0,
|
|
6399
|
+
if ((0, import_fs5.existsSync)((0, import_path4.join)(resolvedPath, "moltspay.services.json"))) {
|
|
6400
|
+
manifestPath = (0, import_path4.join)(resolvedPath, "moltspay.services.json");
|
|
4706
6401
|
skillDir = resolvedPath;
|
|
4707
6402
|
isSkillDir = true;
|
|
4708
6403
|
} else if ((0, import_fs5.existsSync)(resolvedPath) && resolvedPath.endsWith(".json")) {
|
|
4709
6404
|
manifestPath = resolvedPath;
|
|
4710
|
-
skillDir = (0,
|
|
6405
|
+
skillDir = (0, import_path4.dirname)(resolvedPath);
|
|
4711
6406
|
} else if ((0, import_fs5.existsSync)(resolvedPath)) {
|
|
4712
6407
|
console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
|
|
4713
6408
|
continue;
|
|
@@ -4724,7 +6419,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
4724
6419
|
let skillModule = null;
|
|
4725
6420
|
if (isSkillDir) {
|
|
4726
6421
|
let entryPoint = "index.js";
|
|
4727
|
-
const pkgJsonPath = (0,
|
|
6422
|
+
const pkgJsonPath = (0, import_path4.join)(skillDir, "package.json");
|
|
4728
6423
|
if ((0, import_fs5.existsSync)(pkgJsonPath)) {
|
|
4729
6424
|
try {
|
|
4730
6425
|
const pkgJson = JSON.parse((0, import_fs5.readFileSync)(pkgJsonPath, "utf-8"));
|
|
@@ -4734,7 +6429,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
4734
6429
|
} catch {
|
|
4735
6430
|
}
|
|
4736
6431
|
}
|
|
4737
|
-
const modulePath = (0,
|
|
6432
|
+
const modulePath = (0, import_path4.join)(skillDir, entryPoint);
|
|
4738
6433
|
if ((0, import_fs5.existsSync)(modulePath)) {
|
|
4739
6434
|
try {
|
|
4740
6435
|
skillModule = await import(modulePath);
|
|
@@ -4760,7 +6455,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
4760
6455
|
const workdir = skillDir;
|
|
4761
6456
|
handlers.set(service.id, async (params) => {
|
|
4762
6457
|
return new Promise((resolvePromise, reject) => {
|
|
4763
|
-
const proc = (0,
|
|
6458
|
+
const proc = (0, import_child_process3.spawn)("sh", ["-c", service.command], {
|
|
4764
6459
|
cwd: workdir,
|
|
4765
6460
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4766
6461
|
});
|
|
@@ -4813,7 +6508,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
4813
6508
|
provider,
|
|
4814
6509
|
services: allServices
|
|
4815
6510
|
};
|
|
4816
|
-
const tempManifestPath = (0,
|
|
6511
|
+
const tempManifestPath = (0, import_path4.join)(DEFAULT_CONFIG_DIR2, "combined-manifest.json");
|
|
4817
6512
|
(0, import_fs5.writeFileSync)(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
|
|
4818
6513
|
console.log(`
|
|
4819
6514
|
\u{1F4CB} Combined manifest: ${allServices.length} services`);
|
|
@@ -4876,7 +6571,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
4876
6571
|
}
|
|
4877
6572
|
process.kill(pid, "SIGTERM");
|
|
4878
6573
|
console.log("\u2705 Sent SIGTERM to server");
|
|
4879
|
-
await new Promise((
|
|
6574
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
4880
6575
|
try {
|
|
4881
6576
|
process.kill(pid, 0);
|
|
4882
6577
|
console.log("\u26A0\uFE0F Server still running, sending SIGKILL...");
|
|
@@ -4892,9 +6587,10 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
4892
6587
|
process.exit(1);
|
|
4893
6588
|
}
|
|
4894
6589
|
});
|
|
4895
|
-
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--data <json>", "Raw JSON data to send (for custom input formats)").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, tempo_moderato, solana, or solana_devnet).").option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR2).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
|
|
6590
|
+
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--data <json>", "Raw JSON data to send (for custom input formats)").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, tempo_moderato, solana, or solana_devnet).").option("--rail <rail>", 'Payment rail: a chain name, or "alipay" for the Alipay fiat rail (CNY via alipay-bot)').option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR2).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
|
|
4896
6591
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
4897
|
-
|
|
6592
|
+
const useAlipay = options.rail?.toLowerCase() === "alipay";
|
|
6593
|
+
if (!useAlipay && !client.isInitialized) {
|
|
4898
6594
|
console.error("\u274C Wallet not initialized. Run: npx moltspay init");
|
|
4899
6595
|
process.exit(1);
|
|
4900
6596
|
}
|
|
@@ -4922,7 +6618,7 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
4922
6618
|
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
4923
6619
|
params.image_url = imagePath;
|
|
4924
6620
|
} else {
|
|
4925
|
-
const filePath = (0,
|
|
6621
|
+
const filePath = (0, import_path4.resolve)(imagePath);
|
|
4926
6622
|
if (!(0, import_fs5.existsSync)(filePath)) {
|
|
4927
6623
|
console.error(`\u274C Image file not found: ${filePath}`);
|
|
4928
6624
|
process.exit(1);
|
|
@@ -4939,7 +6635,7 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
4939
6635
|
}
|
|
4940
6636
|
const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
|
|
4941
6637
|
const token = (options.token || "USDC").toUpperCase();
|
|
4942
|
-
if (token === "USDT") {
|
|
6638
|
+
if (!useAlipay && token === "USDT") {
|
|
4943
6639
|
const balance = await client.getBalance();
|
|
4944
6640
|
if (balance.native < 1e-4) {
|
|
4945
6641
|
console.log("\n\u26A0\uFE0F USDT requires a small amount of ETH for gas (~$0.01)");
|
|
@@ -4963,13 +6659,31 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
4963
6659
|
console.log(` Prompt: ${params.prompt}`);
|
|
4964
6660
|
}
|
|
4965
6661
|
if (imageDisplay) console.log(` Image: ${imageDisplay}`);
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
6662
|
+
if (useAlipay) {
|
|
6663
|
+
console.log(` Rail: alipay (CNY via alipay-bot)`);
|
|
6664
|
+
} else {
|
|
6665
|
+
console.log(` Chain: ${chain || "(auto)"}`);
|
|
6666
|
+
console.log(` Token: ${token}`);
|
|
6667
|
+
console.log(` Wallet: ${client.address}`);
|
|
6668
|
+
}
|
|
4969
6669
|
console.log("");
|
|
4970
6670
|
}
|
|
4971
6671
|
try {
|
|
4972
|
-
const result = await client.pay(server, service, params, {
|
|
6672
|
+
const result = await client.pay(server, service, params, useAlipay ? {
|
|
6673
|
+
rail: "alipay",
|
|
6674
|
+
rawData: useRawData,
|
|
6675
|
+
onPaymentPending: ({ paymentUrl, shortenUrl }) => {
|
|
6676
|
+
if (!options.json) {
|
|
6677
|
+
process.stdout.write(`
|
|
6678
|
+
\u{1F4F2} \u8BF7\u7528\u652F\u4ED8\u5B9D\u626B\u7801\u6216\u8BBF\u95EE\uFF1A${shortenUrl ?? paymentUrl}
|
|
6679
|
+
|
|
6680
|
+
`);
|
|
6681
|
+
}
|
|
6682
|
+
},
|
|
6683
|
+
onLine: (line) => {
|
|
6684
|
+
if (!options.json) process.stdout.write(line + "\n");
|
|
6685
|
+
}
|
|
6686
|
+
} : {
|
|
4973
6687
|
token,
|
|
4974
6688
|
chain,
|
|
4975
6689
|
rawData: useRawData
|
|
@@ -4990,11 +6704,34 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
4990
6704
|
process.exit(1);
|
|
4991
6705
|
}
|
|
4992
6706
|
});
|
|
6707
|
+
program.command("alipay <action> [args...]").description("Alipay wallet setup via alipay-bot: check | apply | bind").allowUnknownOption().action(async (action, args) => {
|
|
6708
|
+
const map = {
|
|
6709
|
+
check: "check-wallet",
|
|
6710
|
+
apply: "apply-wallet",
|
|
6711
|
+
bind: "bind-wallet"
|
|
6712
|
+
};
|
|
6713
|
+
const sub = map[action] ?? action;
|
|
6714
|
+
const child = (0, import_child_process3.spawn)("alipay-bot", [sub, ...args ?? []], {
|
|
6715
|
+
stdio: "inherit",
|
|
6716
|
+
env: filterEnv(process.env)
|
|
6717
|
+
});
|
|
6718
|
+
child.on("error", (e) => {
|
|
6719
|
+
if (e?.code === "ENOENT") {
|
|
6720
|
+
console.error(
|
|
6721
|
+
"\u274C alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
|
|
6722
|
+
);
|
|
6723
|
+
} else {
|
|
6724
|
+
console.error(`\u274C ${e.message}`);
|
|
6725
|
+
}
|
|
6726
|
+
process.exit(1);
|
|
6727
|
+
});
|
|
6728
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
6729
|
+
});
|
|
4993
6730
|
program.command("validate <path>").description("Validate a moltspay.services.json file against the schema").action(async (inputPath) => {
|
|
4994
|
-
const resolvedPath = (0,
|
|
6731
|
+
const resolvedPath = (0, import_path4.resolve)(inputPath);
|
|
4995
6732
|
let manifestPath;
|
|
4996
|
-
if ((0, import_fs5.existsSync)((0,
|
|
4997
|
-
manifestPath = (0,
|
|
6733
|
+
if ((0, import_fs5.existsSync)((0, import_path4.join)(resolvedPath, "moltspay.services.json"))) {
|
|
6734
|
+
manifestPath = (0, import_path4.join)(resolvedPath, "moltspay.services.json");
|
|
4998
6735
|
} else if (resolvedPath.endsWith(".json") && (0, import_fs5.existsSync)(resolvedPath)) {
|
|
4999
6736
|
manifestPath = resolvedPath;
|
|
5000
6737
|
} else {
|