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.mjs
CHANGED
|
@@ -21,18 +21,21 @@ var init_esm_shims = __esm({
|
|
|
21
21
|
init_esm_shims();
|
|
22
22
|
import { webcrypto } from "crypto";
|
|
23
23
|
import { Command } from "commander";
|
|
24
|
-
import { homedir as
|
|
25
|
-
import { join as
|
|
24
|
+
import { homedir as homedir4 } from "os";
|
|
25
|
+
import { join as join6, dirname as dirname2, resolve as resolve2 } from "path";
|
|
26
26
|
import { existsSync as existsSync5, writeFileSync as writeFileSync3, readFileSync as readFileSync5, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
|
|
27
|
-
import { spawn } from "child_process";
|
|
28
|
-
import { ethers as
|
|
27
|
+
import { spawn as spawn2 } from "child_process";
|
|
28
|
+
import { ethers as ethers4 } from "ethers";
|
|
29
29
|
|
|
30
30
|
// src/client/index.ts
|
|
31
31
|
init_esm_shims();
|
|
32
|
+
|
|
33
|
+
// src/client/node/index.ts
|
|
34
|
+
init_esm_shims();
|
|
32
35
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
|
|
33
|
-
import { homedir as
|
|
34
|
-
import { join as
|
|
35
|
-
import { Wallet, ethers } from "ethers";
|
|
36
|
+
import { homedir as homedir3 } from "os";
|
|
37
|
+
import { join as join3 } from "path";
|
|
38
|
+
import { Wallet as Wallet2, ethers as ethers2 } from "ethers";
|
|
36
39
|
|
|
37
40
|
// src/chains/index.ts
|
|
38
41
|
init_esm_shims();
|
|
@@ -208,6 +211,10 @@ function getChain(name) {
|
|
|
208
211
|
}
|
|
209
212
|
return config;
|
|
210
213
|
}
|
|
214
|
+
var ALIPAY_CHAIN_ID = "alipay";
|
|
215
|
+
function isAlipayChainId(id) {
|
|
216
|
+
return id === ALIPAY_CHAIN_ID;
|
|
217
|
+
}
|
|
211
218
|
|
|
212
219
|
// src/wallet/solana.ts
|
|
213
220
|
init_esm_shims();
|
|
@@ -526,16 +533,16 @@ var SolanaFacilitator = class extends BaseFacilitator {
|
|
|
526
533
|
return this.supportedNetworks.includes(network);
|
|
527
534
|
}
|
|
528
535
|
};
|
|
529
|
-
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
536
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey, connection) {
|
|
530
537
|
const chainConfig = SOLANA_CHAINS[chain];
|
|
531
|
-
const
|
|
538
|
+
const conn = connection ?? new Connection3(chainConfig.rpc, "confirmed");
|
|
532
539
|
const mint = new PublicKey3(chainConfig.tokens.USDC.mint);
|
|
533
540
|
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
534
541
|
const senderATA = await getAssociatedTokenAddress2(mint, senderPubkey);
|
|
535
542
|
const recipientATA = await getAssociatedTokenAddress2(mint, recipientPubkey);
|
|
536
543
|
const transaction = new Transaction();
|
|
537
544
|
try {
|
|
538
|
-
await getAccount2(
|
|
545
|
+
await getAccount2(conn, recipientATA);
|
|
539
546
|
} catch {
|
|
540
547
|
transaction.add(
|
|
541
548
|
createAssociatedTokenAccountInstruction(
|
|
@@ -566,22 +573,944 @@ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amo
|
|
|
566
573
|
// decimals
|
|
567
574
|
)
|
|
568
575
|
);
|
|
569
|
-
const { blockhash, lastValidBlockHeight } = await
|
|
576
|
+
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash();
|
|
570
577
|
transaction.recentBlockhash = blockhash;
|
|
571
578
|
transaction.feePayer = actualFeePayer;
|
|
572
579
|
return transaction;
|
|
573
580
|
}
|
|
574
581
|
|
|
575
|
-
// src/client/index.ts
|
|
582
|
+
// src/client/node/index.ts
|
|
576
583
|
import { PublicKey as PublicKey4 } from "@solana/web3.js";
|
|
577
584
|
|
|
578
|
-
// src/client/
|
|
585
|
+
// src/client/core/index.ts
|
|
579
586
|
init_esm_shims();
|
|
580
587
|
|
|
581
|
-
// src/client/
|
|
588
|
+
// src/client/core/types.ts
|
|
589
|
+
init_esm_shims();
|
|
582
590
|
var X402_VERSION = 2;
|
|
583
591
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
584
592
|
var PAYMENT_HEADER = "x-payment";
|
|
593
|
+
|
|
594
|
+
// src/client/core/chain-map.ts
|
|
595
|
+
init_esm_shims();
|
|
596
|
+
var NETWORK_TO_CHAIN = {
|
|
597
|
+
"eip155:8453": "base",
|
|
598
|
+
"eip155:137": "polygon",
|
|
599
|
+
"eip155:84532": "base_sepolia",
|
|
600
|
+
"eip155:42431": "tempo_moderato",
|
|
601
|
+
"eip155:56": "bnb",
|
|
602
|
+
"eip155:97": "bnb_testnet",
|
|
603
|
+
"solana:mainnet": "solana",
|
|
604
|
+
"solana:devnet": "solana_devnet"
|
|
605
|
+
};
|
|
606
|
+
var CHAIN_TO_NETWORK = Object.fromEntries(
|
|
607
|
+
Object.entries(NETWORK_TO_CHAIN).map(([network, chain]) => [chain, network])
|
|
608
|
+
);
|
|
609
|
+
function networkToChainName(network) {
|
|
610
|
+
return NETWORK_TO_CHAIN[network] ?? null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/client/core/base64.ts
|
|
614
|
+
init_esm_shims();
|
|
615
|
+
var BufferCtor = globalThis.Buffer;
|
|
616
|
+
|
|
617
|
+
// src/client/core/errors.ts
|
|
618
|
+
init_esm_shims();
|
|
619
|
+
var MoltsPayError = class extends Error {
|
|
620
|
+
code;
|
|
621
|
+
constructor(code, message) {
|
|
622
|
+
super(message);
|
|
623
|
+
this.name = "MoltsPayError";
|
|
624
|
+
this.code = code;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// src/client/core/eip3009.ts
|
|
629
|
+
init_esm_shims();
|
|
630
|
+
var EIP3009_TYPES = {
|
|
631
|
+
TransferWithAuthorization: [
|
|
632
|
+
{ name: "from", type: "address" },
|
|
633
|
+
{ name: "to", type: "address" },
|
|
634
|
+
{ name: "value", type: "uint256" },
|
|
635
|
+
{ name: "validAfter", type: "uint256" },
|
|
636
|
+
{ name: "validBefore", type: "uint256" },
|
|
637
|
+
{ name: "nonce", type: "bytes32" }
|
|
638
|
+
]
|
|
639
|
+
};
|
|
640
|
+
function buildEIP3009TypedData(args) {
|
|
641
|
+
const validAfter = args.validAfter ?? "0";
|
|
642
|
+
const validBefore = args.validBefore ?? (Math.floor(Date.now() / 1e3) + 3600).toString();
|
|
643
|
+
const authorization = {
|
|
644
|
+
from: args.from,
|
|
645
|
+
to: args.to,
|
|
646
|
+
value: args.value,
|
|
647
|
+
validAfter,
|
|
648
|
+
validBefore,
|
|
649
|
+
nonce: args.nonce
|
|
650
|
+
};
|
|
651
|
+
return {
|
|
652
|
+
domain: {
|
|
653
|
+
name: args.tokenName,
|
|
654
|
+
version: args.tokenVersion,
|
|
655
|
+
chainId: args.chainId,
|
|
656
|
+
verifyingContract: args.tokenAddress
|
|
657
|
+
},
|
|
658
|
+
types: EIP3009_TYPES,
|
|
659
|
+
primaryType: "TransferWithAuthorization",
|
|
660
|
+
message: authorization
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/client/core/eip2612.ts
|
|
665
|
+
init_esm_shims();
|
|
666
|
+
|
|
667
|
+
// src/client/core/bnb-intent.ts
|
|
668
|
+
init_esm_shims();
|
|
669
|
+
var BNB_INTENT_TYPES = {
|
|
670
|
+
PaymentIntent: [
|
|
671
|
+
{ name: "from", type: "address" },
|
|
672
|
+
{ name: "to", type: "address" },
|
|
673
|
+
{ name: "amount", type: "uint256" },
|
|
674
|
+
{ name: "token", type: "address" },
|
|
675
|
+
{ name: "service", type: "string" },
|
|
676
|
+
{ name: "nonce", type: "uint256" },
|
|
677
|
+
{ name: "deadline", type: "uint256" }
|
|
678
|
+
]
|
|
679
|
+
};
|
|
680
|
+
var BNB_DOMAIN_NAME = "MoltsPay";
|
|
681
|
+
var BNB_DOMAIN_VERSION = "1";
|
|
682
|
+
function buildBnbIntentTypedData(args) {
|
|
683
|
+
const intent = {
|
|
684
|
+
from: args.from,
|
|
685
|
+
to: args.to,
|
|
686
|
+
amount: args.amount,
|
|
687
|
+
token: args.tokenAddress,
|
|
688
|
+
service: args.service,
|
|
689
|
+
nonce: args.nonce,
|
|
690
|
+
deadline: args.deadline
|
|
691
|
+
};
|
|
692
|
+
return {
|
|
693
|
+
domain: {
|
|
694
|
+
name: BNB_DOMAIN_NAME,
|
|
695
|
+
version: BNB_DOMAIN_VERSION,
|
|
696
|
+
chainId: args.chainId
|
|
697
|
+
},
|
|
698
|
+
types: BNB_INTENT_TYPES,
|
|
699
|
+
primaryType: "PaymentIntent",
|
|
700
|
+
message: intent
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/client/core/solana-tx.ts
|
|
705
|
+
init_esm_shims();
|
|
706
|
+
|
|
707
|
+
// src/client/core/x402.ts
|
|
708
|
+
init_esm_shims();
|
|
709
|
+
|
|
710
|
+
// src/client/node/signer.ts
|
|
711
|
+
init_esm_shims();
|
|
712
|
+
import { ethers } from "ethers";
|
|
713
|
+
import { Transaction as Transaction2 } from "@solana/web3.js";
|
|
714
|
+
var NodeSigner = class {
|
|
715
|
+
evmWallet;
|
|
716
|
+
getSolanaKeypair;
|
|
717
|
+
constructor(evmWallet, options = {}) {
|
|
718
|
+
this.evmWallet = evmWallet;
|
|
719
|
+
this.getSolanaKeypair = options.getSolanaKeypair ?? (() => null);
|
|
720
|
+
}
|
|
721
|
+
async getEvmAddress() {
|
|
722
|
+
return this.evmWallet.address;
|
|
723
|
+
}
|
|
724
|
+
async getSolanaAddress() {
|
|
725
|
+
const kp = this.getSolanaKeypair();
|
|
726
|
+
return kp ? kp.publicKey.toBase58() : null;
|
|
727
|
+
}
|
|
728
|
+
async signTypedData(envelope) {
|
|
729
|
+
const mutableTypes = {};
|
|
730
|
+
for (const [key, fields] of Object.entries(envelope.types)) {
|
|
731
|
+
mutableTypes[key] = [...fields];
|
|
732
|
+
}
|
|
733
|
+
return this.evmWallet.signTypedData(
|
|
734
|
+
envelope.domain,
|
|
735
|
+
mutableTypes,
|
|
736
|
+
envelope.message
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
async sendEvmTransaction(args) {
|
|
740
|
+
const chain = findChainByChainId(args.chainId);
|
|
741
|
+
if (!chain) {
|
|
742
|
+
throw new Error(`sendEvmTransaction: unknown chainId ${args.chainId}`);
|
|
743
|
+
}
|
|
744
|
+
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
745
|
+
const connected = this.evmWallet.connect(provider);
|
|
746
|
+
const tx = await connected.sendTransaction({
|
|
747
|
+
to: args.to,
|
|
748
|
+
data: args.data,
|
|
749
|
+
value: args.value ? BigInt(args.value) : 0n
|
|
750
|
+
});
|
|
751
|
+
return tx.hash;
|
|
752
|
+
}
|
|
753
|
+
async signSolanaTransaction(args) {
|
|
754
|
+
const kp = this.getSolanaKeypair();
|
|
755
|
+
if (!kp) {
|
|
756
|
+
throw new Error("signSolanaTransaction: no Solana wallet configured");
|
|
757
|
+
}
|
|
758
|
+
const tx = Transaction2.from(Buffer.from(args.transactionBase64, "base64"));
|
|
759
|
+
if (args.partialSign) {
|
|
760
|
+
tx.partialSign(kp);
|
|
761
|
+
} else {
|
|
762
|
+
tx.sign(kp);
|
|
763
|
+
}
|
|
764
|
+
return tx.serialize({ requireAllSignatures: false }).toString("base64");
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
function findChainByChainId(chainId) {
|
|
768
|
+
for (const cfg of Object.values(CHAINS)) {
|
|
769
|
+
if (cfg.chainId === chainId) {
|
|
770
|
+
return cfg;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return void 0;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/client/alipay/index.ts
|
|
777
|
+
init_esm_shims();
|
|
778
|
+
import { randomUUID } from "crypto";
|
|
779
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
780
|
+
import { homedir as homedir2 } from "os";
|
|
781
|
+
import { join as join2 } from "path";
|
|
782
|
+
|
|
783
|
+
// src/client/alipay/cli.ts
|
|
784
|
+
init_esm_shims();
|
|
785
|
+
import { spawn } from "child_process";
|
|
786
|
+
|
|
787
|
+
// src/client/alipay/log.ts
|
|
788
|
+
init_esm_shims();
|
|
789
|
+
var RANK = { off: 0, info: 1, debug: 2 };
|
|
790
|
+
function normalizeLevel(v) {
|
|
791
|
+
const s = (v ?? "").toLowerCase();
|
|
792
|
+
return s === "info" || s === "debug" ? s : "off";
|
|
793
|
+
}
|
|
794
|
+
var level = normalizeLevel(process.env.MOLTSPAY_ALIPAY_LOG);
|
|
795
|
+
var sink = (line) => {
|
|
796
|
+
process.stderr.write(line + "\n");
|
|
797
|
+
};
|
|
798
|
+
function enabledFor(want) {
|
|
799
|
+
return RANK[level] >= RANK[want];
|
|
800
|
+
}
|
|
801
|
+
function render(event, fields) {
|
|
802
|
+
const parts = ["[moltspay:alipay]", String(fields.ts), event];
|
|
803
|
+
if (fields.flow) parts.push(`flow=${fields.flow}`);
|
|
804
|
+
if (fields.step) parts.push(`step=${fields.step}`);
|
|
805
|
+
if (typeof fields.ms === "number") parts.push(`${fields.ms}ms`);
|
|
806
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
807
|
+
if (["ts", "level", "event", "flow", "step", "ms"].includes(k)) continue;
|
|
808
|
+
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
809
|
+
}
|
|
810
|
+
return parts.join(" ");
|
|
811
|
+
}
|
|
812
|
+
function emit(lvl, event, fields) {
|
|
813
|
+
if (!enabledFor(lvl)) return;
|
|
814
|
+
const full = { ts: (/* @__PURE__ */ new Date()).toISOString(), level: lvl, event, ...fields };
|
|
815
|
+
try {
|
|
816
|
+
sink(render(event, full), full);
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
var alipayLog = {
|
|
821
|
+
info: (event, fields = {}) => emit("info", event, fields),
|
|
822
|
+
debug: (event, fields = {}) => emit("debug", event, fields),
|
|
823
|
+
/** True if anything at all would be logged (cheap guard for hot paths). */
|
|
824
|
+
enabled: () => level !== "off"
|
|
825
|
+
};
|
|
826
|
+
async function timeStep(step, flow, fn) {
|
|
827
|
+
if (!alipayLog.enabled()) return fn();
|
|
828
|
+
const t0 = Date.now();
|
|
829
|
+
alipayLog.debug("step.start", { flow, step });
|
|
830
|
+
try {
|
|
831
|
+
const out = await fn();
|
|
832
|
+
alipayLog.info("step.end", { flow, step, ms: Date.now() - t0, ok: true });
|
|
833
|
+
return out;
|
|
834
|
+
} catch (err) {
|
|
835
|
+
alipayLog.info("step.end", {
|
|
836
|
+
flow,
|
|
837
|
+
step,
|
|
838
|
+
ms: Date.now() - t0,
|
|
839
|
+
ok: false,
|
|
840
|
+
err: err instanceof Error ? err.message : String(err)
|
|
841
|
+
});
|
|
842
|
+
throw err;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/client/alipay/cli.ts
|
|
847
|
+
var ALLOWED_ENV = /* @__PURE__ */ new Set([
|
|
848
|
+
"AIPAY_OUTPUT_CHANNEL",
|
|
849
|
+
"AIPAY_SESSION_ID",
|
|
850
|
+
"AIPAY_FRAMEWORK",
|
|
851
|
+
"AIPAY_MODEL",
|
|
852
|
+
"AIPAY_OS",
|
|
853
|
+
// Minimal survival set for spawn to find the binary and a home dir.
|
|
854
|
+
"PATH",
|
|
855
|
+
"HOME"
|
|
856
|
+
]);
|
|
857
|
+
function filterEnv(env) {
|
|
858
|
+
return Object.fromEntries(
|
|
859
|
+
Object.entries(env).filter(([k]) => ALLOWED_ENV.has(k))
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
function makeLineSplitter(onLine) {
|
|
863
|
+
let buf = "";
|
|
864
|
+
return (chunk) => {
|
|
865
|
+
buf += chunk.toString("utf-8");
|
|
866
|
+
let nl;
|
|
867
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
868
|
+
onLine(buf.slice(0, nl));
|
|
869
|
+
buf = buf.slice(nl + 1);
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function profileEnv(step, flow) {
|
|
874
|
+
const want = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE;
|
|
875
|
+
const hook = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_HOOK;
|
|
876
|
+
if (!want || !hook) return {};
|
|
877
|
+
const steps = want.split(",").map((s) => s.trim());
|
|
878
|
+
if (!steps.includes("all") && !steps.includes(step)) return {};
|
|
879
|
+
const dir = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_DIR || "/tmp";
|
|
880
|
+
const safe = `${flow ?? "noflow"}-${step}-${Date.now()}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
881
|
+
const out = `${dir}/cli-profile-${safe}.json`;
|
|
882
|
+
alipayLog.info("cli.profile", { flow, step, out });
|
|
883
|
+
return {
|
|
884
|
+
NODE_OPTIONS: `--require=${hook}`,
|
|
885
|
+
MOLTSPAY_CLI_PROFILE_OUT: out
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
var runCli = (args, opts = {}) => {
|
|
889
|
+
const bin = opts.bin ?? "alipay-bot";
|
|
890
|
+
const step = opts.step ?? args[0] ?? "(no-arg)";
|
|
891
|
+
const startedAt = Date.now();
|
|
892
|
+
const lines = [];
|
|
893
|
+
const collect = (line) => {
|
|
894
|
+
lines.push(line);
|
|
895
|
+
alipayLog.debug("cli.line", {
|
|
896
|
+
flow: opts.flow,
|
|
897
|
+
step,
|
|
898
|
+
ms: Date.now() - startedAt,
|
|
899
|
+
preview: line.slice(0, 120)
|
|
900
|
+
});
|
|
901
|
+
opts.onLine?.(line);
|
|
902
|
+
};
|
|
903
|
+
alipayLog.debug("step.start", { flow: opts.flow, step });
|
|
904
|
+
return new Promise((resolve3, reject) => {
|
|
905
|
+
const child = spawn(bin, args, {
|
|
906
|
+
env: { ...filterEnv(process.env), ...profileEnv(step, opts.flow), ...opts.env ?? {} }
|
|
907
|
+
});
|
|
908
|
+
if (opts.signal) {
|
|
909
|
+
if (opts.signal.aborted) child.kill("SIGTERM");
|
|
910
|
+
opts.signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
|
|
911
|
+
}
|
|
912
|
+
let sawByte = false;
|
|
913
|
+
const onChunk = (stream) => (chunk) => {
|
|
914
|
+
const ms = Date.now() - startedAt;
|
|
915
|
+
if (!sawByte) {
|
|
916
|
+
sawByte = true;
|
|
917
|
+
alipayLog.debug("cli.firstbyte", { flow: opts.flow, step, ms });
|
|
918
|
+
}
|
|
919
|
+
alipayLog.debug("cli.chunk", { flow: opts.flow, step, ms, stream, bytes: chunk.length });
|
|
920
|
+
};
|
|
921
|
+
const onStdout = makeLineSplitter(collect);
|
|
922
|
+
const onStderr = makeLineSplitter(collect);
|
|
923
|
+
child.stdout?.on("data", onChunk("out"));
|
|
924
|
+
child.stderr?.on("data", onChunk("err"));
|
|
925
|
+
child.stdout?.on("data", onStdout);
|
|
926
|
+
child.stderr?.on("data", onStderr);
|
|
927
|
+
child.on("error", (err) => {
|
|
928
|
+
alipayLog.info("cli.exit", {
|
|
929
|
+
flow: opts.flow,
|
|
930
|
+
step,
|
|
931
|
+
ms: Date.now() - startedAt,
|
|
932
|
+
ok: false,
|
|
933
|
+
err: err.message
|
|
934
|
+
});
|
|
935
|
+
reject(err);
|
|
936
|
+
});
|
|
937
|
+
child.on("close", (code) => {
|
|
938
|
+
alipayLog.info("cli.exit", {
|
|
939
|
+
flow: opts.flow,
|
|
940
|
+
step,
|
|
941
|
+
ms: Date.now() - startedAt,
|
|
942
|
+
ok: (code ?? 1) === 0,
|
|
943
|
+
code: code ?? 1
|
|
944
|
+
});
|
|
945
|
+
resolve3({ exitCode: code ?? 1, lines });
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// src/client/alipay/install.ts
|
|
951
|
+
init_esm_shims();
|
|
952
|
+
import { execFile } from "child_process";
|
|
953
|
+
import { promisify } from "util";
|
|
954
|
+
|
|
955
|
+
// src/client/alipay/errors.ts
|
|
956
|
+
init_esm_shims();
|
|
957
|
+
var AlipayCliNotFoundError = class extends MoltsPayError {
|
|
958
|
+
constructor(message) {
|
|
959
|
+
super("ALIPAY_CLI_NOT_FOUND", message);
|
|
960
|
+
this.name = "AlipayCliNotFoundError";
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
var AlipayCliVersionError = class extends MoltsPayError {
|
|
964
|
+
constructor(message) {
|
|
965
|
+
super("ALIPAY_CLI_VERSION", message);
|
|
966
|
+
this.name = "AlipayCliVersionError";
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
var NeedsWalletSetupError = class extends MoltsPayError {
|
|
970
|
+
constructor(message = "Alipay wallet not set up. Run: moltspay alipay apply") {
|
|
971
|
+
super("ALIPAY_NEEDS_WALLET_SETUP", message);
|
|
972
|
+
this.name = "NeedsWalletSetupError";
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
var AlipayPaymentRejectedError = class extends MoltsPayError {
|
|
976
|
+
constructor(message) {
|
|
977
|
+
super("ALIPAY_PAYMENT_REJECTED", message);
|
|
978
|
+
this.name = "AlipayPaymentRejectedError";
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
var AlipayPaymentTimeoutError = class extends MoltsPayError {
|
|
982
|
+
constructor(message) {
|
|
983
|
+
super("ALIPAY_PAYMENT_TIMEOUT", message);
|
|
984
|
+
this.name = "AlipayPaymentTimeoutError";
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
var AlipayProtocolError = class extends MoltsPayError {
|
|
988
|
+
constructor(message) {
|
|
989
|
+
super("ALIPAY_PROTOCOL", message);
|
|
990
|
+
this.name = "AlipayProtocolError";
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
var UnsupportedRailError = class extends MoltsPayError {
|
|
994
|
+
constructor(rail, message) {
|
|
995
|
+
super("UNSUPPORTED_RAIL", message ?? `Rail not supported by server: ${rail}`);
|
|
996
|
+
this.rail = rail;
|
|
997
|
+
this.name = "UnsupportedRailError";
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// src/client/alipay/install.ts
|
|
1002
|
+
var execFileAsync = promisify(execFile);
|
|
1003
|
+
var MIN_CLI_VERSION = "0.3.15";
|
|
1004
|
+
function semverLt(a, b) {
|
|
1005
|
+
const pa = a.split(".").map((n) => parseInt(n, 10));
|
|
1006
|
+
const pb = b.split(".").map((n) => parseInt(n, 10));
|
|
1007
|
+
for (let i = 0; i < 3; i++) {
|
|
1008
|
+
const x = pa[i] ?? 0;
|
|
1009
|
+
const y = pb[i] ?? 0;
|
|
1010
|
+
if (x !== y) return x < y;
|
|
1011
|
+
}
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
function parseVersion(stdout) {
|
|
1015
|
+
return stdout.match(/v?(\d+\.\d+\.\d+)/)?.[1] ?? null;
|
|
1016
|
+
}
|
|
1017
|
+
var defaultGetVersion = async () => {
|
|
1018
|
+
const { stdout } = await execFileAsync("alipay-bot", ["--version"]);
|
|
1019
|
+
return stdout;
|
|
1020
|
+
};
|
|
1021
|
+
async function ensureCliUncached(getVersion2) {
|
|
1022
|
+
let stdout;
|
|
1023
|
+
try {
|
|
1024
|
+
stdout = await getVersion2();
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
if (e?.code === "ENOENT") {
|
|
1027
|
+
throw new AlipayCliNotFoundError(
|
|
1028
|
+
"alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
throw e;
|
|
1032
|
+
}
|
|
1033
|
+
const version = parseVersion(stdout);
|
|
1034
|
+
if (!version || semverLt(version, MIN_CLI_VERSION)) {
|
|
1035
|
+
throw new AlipayCliVersionError(
|
|
1036
|
+
`alipay-bot ${version ?? "?"} found, need \u2265 ${MIN_CLI_VERSION}. Run: npx -y @alipay/agent-payment@latest update`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return version;
|
|
1040
|
+
}
|
|
1041
|
+
var cachedVersion = null;
|
|
1042
|
+
var inflight = null;
|
|
1043
|
+
async function ensureCli(getVersion2 = defaultGetVersion) {
|
|
1044
|
+
if (getVersion2 !== defaultGetVersion) {
|
|
1045
|
+
return ensureCliUncached(getVersion2);
|
|
1046
|
+
}
|
|
1047
|
+
if (cachedVersion) return cachedVersion;
|
|
1048
|
+
if (!inflight) {
|
|
1049
|
+
inflight = ensureCliUncached(getVersion2).then((v) => {
|
|
1050
|
+
cachedVersion = v;
|
|
1051
|
+
return v;
|
|
1052
|
+
}).finally(() => {
|
|
1053
|
+
inflight = null;
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
return inflight;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/client/alipay/poll.ts
|
|
1060
|
+
init_esm_shims();
|
|
1061
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
1062
|
+
var POLL_MAX_INFLIGHT = 2;
|
|
1063
|
+
function resolveMaxInflight(override) {
|
|
1064
|
+
if (override !== void 0) return Math.max(1, Math.floor(override));
|
|
1065
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_MAX_INFLIGHT;
|
|
1066
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
1067
|
+
const n = Number(raw);
|
|
1068
|
+
if (Number.isFinite(n) && n >= 1) return Math.floor(n);
|
|
1069
|
+
}
|
|
1070
|
+
return POLL_MAX_INFLIGHT;
|
|
1071
|
+
}
|
|
1072
|
+
function resolveLaunchGap(override) {
|
|
1073
|
+
if (override !== void 0) return Math.max(0, override);
|
|
1074
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_LAUNCH_MS;
|
|
1075
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
1076
|
+
const n = Number(raw);
|
|
1077
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
1078
|
+
}
|
|
1079
|
+
return POLL_INTERVAL_MS;
|
|
1080
|
+
}
|
|
1081
|
+
function parseStatus(lines) {
|
|
1082
|
+
const raw = lines.join("\n").trim();
|
|
1083
|
+
try {
|
|
1084
|
+
const json = JSON.parse(raw);
|
|
1085
|
+
if (json && typeof json === "object") {
|
|
1086
|
+
if (typeof json.success === "boolean") {
|
|
1087
|
+
if (json.success) return "paid";
|
|
1088
|
+
const err = String(json.errorCode ?? "").toUpperCase();
|
|
1089
|
+
if (/UNPAID|WAIT|PENDING|PROCESS|NOTPAY/.test(err)) return "pending";
|
|
1090
|
+
if (/CLOSED|CANCEL|FAIL|REJECT|REFUSE|TIMEOUT|EXPIRE/.test(err)) return "rejected";
|
|
1091
|
+
return "pending";
|
|
1092
|
+
}
|
|
1093
|
+
if (typeof json.code !== "undefined") {
|
|
1094
|
+
if (Number(json.code) === 200) return "paid";
|
|
1095
|
+
const msg = `${json.message ?? ""}${json.reason ?? ""}`;
|
|
1096
|
+
if (/关闭|失败|拒绝|取消|超时|已撤销/.test(msg)) return "rejected";
|
|
1097
|
+
return "pending";
|
|
1098
|
+
}
|
|
1099
|
+
if (typeof json.body === "string") {
|
|
1100
|
+
return classifyReportText(json.body);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
} catch {
|
|
1104
|
+
}
|
|
1105
|
+
return classifyReportText(raw);
|
|
1106
|
+
}
|
|
1107
|
+
function classifyReportText(s) {
|
|
1108
|
+
const text = s.toUpperCase();
|
|
1109
|
+
if (/查询支付状态成功并获取资源/.test(s) || /资源响应状态[^0-9\n]{0,8}200/.test(s) || /TRADE_SUCCESS|TRADE_FINISHED|"STATUS":\s*"FULFILLED"/.test(text)) return "paid";
|
|
1110
|
+
if (/TRADE_CLOSED|REJECTED|REFUSE|CANCEL/.test(text) || /交易关闭|支付失败|已拒绝|已取消|已撤销|交易超时/.test(s)) return "rejected";
|
|
1111
|
+
if (/WAIT_BUYER_PAY|UNPAID|PENDING|WAITING/.test(text) || /交易未支付|等待付款|待支付|未支付/.test(s)) return "pending";
|
|
1112
|
+
return "unknown";
|
|
1113
|
+
}
|
|
1114
|
+
function defaultSleep(ms, signal) {
|
|
1115
|
+
return new Promise((resolve3, reject) => {
|
|
1116
|
+
if (signal?.aborted) return reject(new Error("aborted"));
|
|
1117
|
+
const t = setTimeout(resolve3, ms);
|
|
1118
|
+
signal?.addEventListener(
|
|
1119
|
+
"abort",
|
|
1120
|
+
() => {
|
|
1121
|
+
clearTimeout(t);
|
|
1122
|
+
reject(new Error("aborted"));
|
|
1123
|
+
},
|
|
1124
|
+
{ once: true }
|
|
1125
|
+
);
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
var LAUNCH = /* @__PURE__ */ Symbol("launch");
|
|
1129
|
+
async function pollUntil(tradeNo, resourceUrl, opts) {
|
|
1130
|
+
const runner = opts.runner ?? runCli;
|
|
1131
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
1132
|
+
const now = opts.now ?? Date.now;
|
|
1133
|
+
const maxInflight = resolveMaxInflight(opts.maxInflight);
|
|
1134
|
+
const launchGap = resolveLaunchGap(opts.launchIntervalMs);
|
|
1135
|
+
const args = [
|
|
1136
|
+
"402-query-payment-status",
|
|
1137
|
+
"-t",
|
|
1138
|
+
tradeNo,
|
|
1139
|
+
"-r",
|
|
1140
|
+
resourceUrl,
|
|
1141
|
+
...opts.method ? ["-m", opts.method] : [],
|
|
1142
|
+
...opts.data ? ["-d", opts.data] : []
|
|
1143
|
+
];
|
|
1144
|
+
const internal = new AbortController();
|
|
1145
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, internal.signal]) : internal.signal;
|
|
1146
|
+
let tick = 0;
|
|
1147
|
+
let lastLaunch = -Infinity;
|
|
1148
|
+
const inflight2 = /* @__PURE__ */ new Set();
|
|
1149
|
+
const launch = () => {
|
|
1150
|
+
tick += 1;
|
|
1151
|
+
const myTick = tick;
|
|
1152
|
+
lastLaunch = now();
|
|
1153
|
+
const run = (async () => {
|
|
1154
|
+
const { lines } = await runner(args, {
|
|
1155
|
+
signal,
|
|
1156
|
+
step: "query-payment-status",
|
|
1157
|
+
flow: tradeNo
|
|
1158
|
+
});
|
|
1159
|
+
const status = parseStatus(lines);
|
|
1160
|
+
alipayLog.debug("poll.tick", { flow: tradeNo, tick: myTick, status });
|
|
1161
|
+
return { status, lines };
|
|
1162
|
+
})();
|
|
1163
|
+
const tracked = run.finally(() => {
|
|
1164
|
+
inflight2.delete(tracked);
|
|
1165
|
+
});
|
|
1166
|
+
inflight2.add(tracked);
|
|
1167
|
+
};
|
|
1168
|
+
try {
|
|
1169
|
+
for (; ; ) {
|
|
1170
|
+
if (opts.signal?.aborted) throw new Error("aborted");
|
|
1171
|
+
const expired = now() >= opts.deadline;
|
|
1172
|
+
if (expired && inflight2.size === 0) {
|
|
1173
|
+
throw new AlipayPaymentTimeoutError(
|
|
1174
|
+
`Payment ${tradeNo} not completed before pay_before deadline`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
while (!expired && inflight2.size < maxInflight && now() - lastLaunch >= launchGap) {
|
|
1178
|
+
launch();
|
|
1179
|
+
}
|
|
1180
|
+
const waiters = [...inflight2];
|
|
1181
|
+
if (!expired && inflight2.size < maxInflight) {
|
|
1182
|
+
const untilNext = Math.max(0, launchGap - (now() - lastLaunch));
|
|
1183
|
+
waiters.push(sleep(untilNext, signal).then(() => LAUNCH, () => LAUNCH));
|
|
1184
|
+
}
|
|
1185
|
+
const winner = await Promise.race(waiters);
|
|
1186
|
+
if (winner === LAUNCH) continue;
|
|
1187
|
+
if (winner.status === "paid") return { status: "paid", lines: winner.lines };
|
|
1188
|
+
if (winner.status === "rejected") {
|
|
1189
|
+
throw new AlipayPaymentRejectedError(`Payment ${tradeNo} was rejected`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
} finally {
|
|
1193
|
+
internal.abort();
|
|
1194
|
+
for (const p of inflight2) p.catch(() => void 0);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/client/alipay/index.ts
|
|
1199
|
+
var TRADE_NO_RE = /^\d{32}$/;
|
|
1200
|
+
function resolveSessionId(explicit, envSession) {
|
|
1201
|
+
return explicit ?? envSession ?? randomUUID();
|
|
1202
|
+
}
|
|
1203
|
+
function assertTradeNo(t) {
|
|
1204
|
+
if (!TRADE_NO_RE.test(t)) {
|
|
1205
|
+
throw new AlipayProtocolError(`invalid tradeNo (expect 32 digits): ${t}`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function parseTradeNo(lines) {
|
|
1209
|
+
for (const line of lines) {
|
|
1210
|
+
const labeled = line.match(/trade[_-]?no["'\s:=]+(\d{32})/i);
|
|
1211
|
+
if (labeled) return labeled[1];
|
|
1212
|
+
const bare = line.match(/\b(\d{32})\b/);
|
|
1213
|
+
if (bare) return bare[1];
|
|
1214
|
+
}
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
function parsePaymentUrl(lines) {
|
|
1218
|
+
let paymentUrl;
|
|
1219
|
+
let shortenUrl;
|
|
1220
|
+
for (const line of lines) {
|
|
1221
|
+
const m = line.match(/(alipays?:\/\/\S+|https?:\/\/\S+)/i);
|
|
1222
|
+
if (!m) continue;
|
|
1223
|
+
const url = m[1].replace(/[)\]`>]+$/, "");
|
|
1224
|
+
if (/short|qr\.alipay|surl|\/s\//i.test(line) && !shortenUrl) shortenUrl = url;
|
|
1225
|
+
else if (!paymentUrl) paymentUrl = url;
|
|
1226
|
+
}
|
|
1227
|
+
if (!paymentUrl && shortenUrl) paymentUrl = shortenUrl;
|
|
1228
|
+
return { paymentUrl, shortenUrl };
|
|
1229
|
+
}
|
|
1230
|
+
function extractMedia(line) {
|
|
1231
|
+
const m = line.match(/^\s*MEDIA:\s*(.+?)\s*$/);
|
|
1232
|
+
return m ? m[1] : null;
|
|
1233
|
+
}
|
|
1234
|
+
function isWalletReady(lines) {
|
|
1235
|
+
const text = lines.join("\n").trim();
|
|
1236
|
+
try {
|
|
1237
|
+
const json = JSON.parse(text);
|
|
1238
|
+
if (json && typeof json.code !== "undefined") return Number(json.code) === 200;
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1241
|
+
return !/未开通|未开启|NOT[_\s-]*(OPEN|BOUND|SET)|NEEDS?[_\s-]*SETUP|NO[_\s-]*WALLET/i.test(text);
|
|
1242
|
+
}
|
|
1243
|
+
function extractResourceFromReport(report) {
|
|
1244
|
+
const label = report.search(/资源响应体/);
|
|
1245
|
+
const start = report.indexOf("{", label === -1 ? 0 : label);
|
|
1246
|
+
if (start === -1) return void 0;
|
|
1247
|
+
let depth = 0, inStr = false, esc = false;
|
|
1248
|
+
for (let i = start; i < report.length; i++) {
|
|
1249
|
+
const ch = report[i];
|
|
1250
|
+
if (inStr) {
|
|
1251
|
+
if (esc) esc = false;
|
|
1252
|
+
else if (ch === "\\") esc = true;
|
|
1253
|
+
else if (ch === '"') inStr = false;
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
if (ch === '"') inStr = true;
|
|
1257
|
+
else if (ch === "{") depth++;
|
|
1258
|
+
else if (ch === "}" && --depth === 0) {
|
|
1259
|
+
const slice = report.slice(start, i + 1);
|
|
1260
|
+
try {
|
|
1261
|
+
const obj = JSON.parse(slice);
|
|
1262
|
+
const r = obj?.resourceResponse ?? obj?.result ?? obj?.data ?? obj?.body ?? obj;
|
|
1263
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
1264
|
+
} catch {
|
|
1265
|
+
return slice;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return void 0;
|
|
1270
|
+
}
|
|
1271
|
+
function extractBody(lines) {
|
|
1272
|
+
const text = lines.join("\n").trim();
|
|
1273
|
+
try {
|
|
1274
|
+
const json = JSON.parse(text);
|
|
1275
|
+
if (json && typeof json === "object") {
|
|
1276
|
+
if (json.resourceResponse !== void 0 && json.resourceResponse !== null) {
|
|
1277
|
+
const rr = json.resourceResponse;
|
|
1278
|
+
const r = typeof rr === "object" ? rr.result ?? rr.data ?? rr.body ?? rr : rr;
|
|
1279
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
1280
|
+
}
|
|
1281
|
+
if (typeof json.body === "string") {
|
|
1282
|
+
const inner = extractResourceFromReport(json.body);
|
|
1283
|
+
return inner ?? json.body;
|
|
1284
|
+
}
|
|
1285
|
+
const body = json.data ?? json.result ?? json.body ?? json.resource;
|
|
1286
|
+
if (body !== void 0) return typeof body === "string" ? body : JSON.stringify(body);
|
|
1287
|
+
return text;
|
|
1288
|
+
}
|
|
1289
|
+
} catch {
|
|
1290
|
+
}
|
|
1291
|
+
const idx = lines.findIndex((l) => /^\s*BODY:/.test(l));
|
|
1292
|
+
if (idx !== -1) {
|
|
1293
|
+
const first = lines[idx].replace(/^\s*BODY:\s*/, "");
|
|
1294
|
+
return [first, ...lines.slice(idx + 1)].join("\n").trim();
|
|
1295
|
+
}
|
|
1296
|
+
return lines.filter((l) => !/^\s*(MEDIA|STATUS|INFO):/.test(l)).join("\n").trim();
|
|
1297
|
+
}
|
|
1298
|
+
var DEFAULT_WALLET_TTL_MS = 10 * 60 * 1e3;
|
|
1299
|
+
function resolveWalletTtlMs() {
|
|
1300
|
+
const raw = process.env.MOLTSPAY_ALIPAY_WALLET_TTL_MS;
|
|
1301
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_WALLET_TTL_MS;
|
|
1302
|
+
const n = Number(raw);
|
|
1303
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_WALLET_TTL_MS;
|
|
1304
|
+
}
|
|
1305
|
+
var walletReadyUntil = /* @__PURE__ */ new Map();
|
|
1306
|
+
var DEFAULT_INTENT_TTL_MS = 10 * 60 * 1e3;
|
|
1307
|
+
function resolveIntentTtlMs() {
|
|
1308
|
+
const raw = process.env.MOLTSPAY_ALIPAY_INTENT_TTL_MS;
|
|
1309
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_INTENT_TTL_MS;
|
|
1310
|
+
const n = Number(raw);
|
|
1311
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTENT_TTL_MS;
|
|
1312
|
+
}
|
|
1313
|
+
var intentDoneUntil = /* @__PURE__ */ new Map();
|
|
1314
|
+
var AlipayClient = class {
|
|
1315
|
+
sessionId;
|
|
1316
|
+
configDir;
|
|
1317
|
+
framework;
|
|
1318
|
+
runner;
|
|
1319
|
+
getVersion;
|
|
1320
|
+
now;
|
|
1321
|
+
/** Only the default runner may use the process-level wallet cache. */
|
|
1322
|
+
walletCacheable;
|
|
1323
|
+
/** Only the default runner may use the process-level payment-intent cache. */
|
|
1324
|
+
intentCacheable;
|
|
1325
|
+
constructor(opts = {}) {
|
|
1326
|
+
this.sessionId = resolveSessionId(opts.sessionId, process.env.AIPAY_SESSION_ID);
|
|
1327
|
+
this.configDir = opts.configDir ?? join2(homedir2(), ".moltspay");
|
|
1328
|
+
this.framework = opts.framework ?? process.env.AIPAY_FRAMEWORK ?? "openclaw";
|
|
1329
|
+
this.runner = opts.runner ?? runCli;
|
|
1330
|
+
this.getVersion = opts.getVersion;
|
|
1331
|
+
this.now = opts.now ?? Date.now;
|
|
1332
|
+
this.walletCacheable = opts.cacheWallet ?? !opts.runner;
|
|
1333
|
+
this.intentCacheable = opts.cacheIntent ?? !opts.runner;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Throws NeedsWalletSetupError unless alipay-bot reports an opened wallet.
|
|
1337
|
+
*
|
|
1338
|
+
* Skips the ~22s `check-wallet` spawn entirely when a prior call cached a
|
|
1339
|
+
* "ready" verdict within the TTL (see {@link DEFAULT_WALLET_TTL_MS}).
|
|
1340
|
+
*/
|
|
1341
|
+
async checkWallet(signal, flow) {
|
|
1342
|
+
const ttlMs = resolveWalletTtlMs();
|
|
1343
|
+
const cacheKey = `${this.configDir}::${this.framework}`;
|
|
1344
|
+
const useCache = this.walletCacheable && ttlMs > 0;
|
|
1345
|
+
if (useCache) {
|
|
1346
|
+
const until = walletReadyUntil.get(cacheKey);
|
|
1347
|
+
if (until !== void 0 && this.now() < until) {
|
|
1348
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: true, ttlMsLeft: until - this.now() });
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const { lines } = await this.runner(["check-wallet"], { signal, step: "check-wallet", flow });
|
|
1353
|
+
if (!isWalletReady(lines)) {
|
|
1354
|
+
if (this.walletCacheable) walletReadyUntil.delete(cacheKey);
|
|
1355
|
+
throw new NeedsWalletSetupError(
|
|
1356
|
+
"Alipay wallet not opened. Run: moltspay alipay apply (then: moltspay alipay bind)"
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
if (useCache) {
|
|
1360
|
+
walletReadyUntil.set(cacheKey, this.now() + ttlMs);
|
|
1361
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: false, cachedForMs: ttlMs });
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
/** Run the full 8-step flow and resolve with the resource body. */
|
|
1365
|
+
async pay402(opts) {
|
|
1366
|
+
const { resourceUrl, requirement, signal } = opts;
|
|
1367
|
+
const extra = requirement.extra ?? {};
|
|
1368
|
+
const paymentNeededHeader = String(extra.payment_needed_header ?? "");
|
|
1369
|
+
if (!paymentNeededHeader) {
|
|
1370
|
+
throw new AlipayProtocolError("alipay requirement missing extra.payment_needed_header");
|
|
1371
|
+
}
|
|
1372
|
+
const flow = this.sessionId;
|
|
1373
|
+
const flowStart = this.now();
|
|
1374
|
+
alipayLog.info("flow.start", { flow, resource: resourceUrl });
|
|
1375
|
+
const media = [];
|
|
1376
|
+
const onLine = (line) => {
|
|
1377
|
+
const m = extractMedia(line);
|
|
1378
|
+
if (m) media.push(m);
|
|
1379
|
+
else opts.onLine?.(line);
|
|
1380
|
+
};
|
|
1381
|
+
const intentSummary = opts.intentSummary?.trim() || `\u652F\u4ED8 ${requirement.amount ?? ""} ${requirement.asset ?? "CNY"}`.trim();
|
|
1382
|
+
await timeStep("ensure-cli", flow, () => ensureCli(this.getVersion));
|
|
1383
|
+
const intentTtlMs = resolveIntentTtlMs();
|
|
1384
|
+
const intentKey = `${this.configDir}::${this.framework}`;
|
|
1385
|
+
const useIntentCache = this.intentCacheable && intentTtlMs > 0;
|
|
1386
|
+
const intentUntil = useIntentCache ? intentDoneUntil.get(intentKey) : void 0;
|
|
1387
|
+
if (intentUntil !== void 0 && this.now() < intentUntil) {
|
|
1388
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: true, ttlMsLeft: intentUntil - this.now() });
|
|
1389
|
+
} else {
|
|
1390
|
+
await this.runner(
|
|
1391
|
+
[
|
|
1392
|
+
"payment-intent",
|
|
1393
|
+
"--session-id",
|
|
1394
|
+
this.sessionId,
|
|
1395
|
+
"--intent-summary",
|
|
1396
|
+
intentSummary,
|
|
1397
|
+
"--framework",
|
|
1398
|
+
this.framework
|
|
1399
|
+
],
|
|
1400
|
+
{ onLine, signal, step: "payment-intent", flow }
|
|
1401
|
+
);
|
|
1402
|
+
if (useIntentCache) {
|
|
1403
|
+
intentDoneUntil.set(intentKey, this.now() + intentTtlMs);
|
|
1404
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: false, cachedForMs: intentTtlMs });
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
await this.checkWallet(signal, flow);
|
|
1408
|
+
const reqId = String(extra.out_trade_no ?? randomUUID());
|
|
1409
|
+
const dir = join2(this.configDir, "alipay");
|
|
1410
|
+
await mkdir(dir, { recursive: true });
|
|
1411
|
+
const challengeFile = join2(dir, `402_${reqId}.txt`);
|
|
1412
|
+
await writeFile(challengeFile, paymentNeededHeader, "utf-8");
|
|
1413
|
+
const payArgs = [
|
|
1414
|
+
"402-buyer-pay",
|
|
1415
|
+
"-f",
|
|
1416
|
+
challengeFile,
|
|
1417
|
+
"-r",
|
|
1418
|
+
resourceUrl,
|
|
1419
|
+
"-s",
|
|
1420
|
+
this.sessionId,
|
|
1421
|
+
"-i",
|
|
1422
|
+
intentSummary,
|
|
1423
|
+
"-w",
|
|
1424
|
+
this.framework
|
|
1425
|
+
];
|
|
1426
|
+
if (opts.method) payArgs.push("-m", opts.method);
|
|
1427
|
+
if (opts.data) payArgs.push("-d", opts.data);
|
|
1428
|
+
const payRun = await this.runner(payArgs, { onLine, signal, step: "402-buyer-pay", flow });
|
|
1429
|
+
const tradeNo = parseTradeNo(payRun.lines);
|
|
1430
|
+
if (!tradeNo) {
|
|
1431
|
+
throw new AlipayProtocolError("402-buyer-pay did not return a tradeNo");
|
|
1432
|
+
}
|
|
1433
|
+
assertTradeNo(tradeNo);
|
|
1434
|
+
const { paymentUrl, shortenUrl } = parsePaymentUrl(payRun.lines);
|
|
1435
|
+
if (paymentUrl) {
|
|
1436
|
+
alipayLog.info("flow.pending", { flow, tradeNo, ms: this.now() - flowStart });
|
|
1437
|
+
opts.onPaymentPending?.({ paymentUrl, shortenUrl, tradeNo });
|
|
1438
|
+
}
|
|
1439
|
+
const pendingAt = this.now();
|
|
1440
|
+
const windowMs = (requirement.maxTimeoutSeconds ?? 30 * 60) * 1e3;
|
|
1441
|
+
const deadline = this.now() + (opts.timeoutMs ?? windowMs);
|
|
1442
|
+
const poll = await pollUntil(tradeNo, resourceUrl, {
|
|
1443
|
+
// No onLine: the status-poll output embeds the resource and must not reach
|
|
1444
|
+
// the log stream — the body is surfaced via the return value below (§9.3).
|
|
1445
|
+
deadline,
|
|
1446
|
+
signal,
|
|
1447
|
+
runner: this.runner,
|
|
1448
|
+
now: this.now,
|
|
1449
|
+
// Re-fetch the resource the same way it was paid (POST + body), else the
|
|
1450
|
+
// status poll defaults to GET and 404s on a POST-only `/execute`.
|
|
1451
|
+
method: opts.method,
|
|
1452
|
+
data: opts.data
|
|
1453
|
+
});
|
|
1454
|
+
alipayLog.info("flow.settled", { flow, tradeNo, ms: this.now() - pendingAt });
|
|
1455
|
+
const body = extractBody(poll.lines);
|
|
1456
|
+
void this.runner(["402-buyer-fulfillment-ack", "-t", tradeNo], {
|
|
1457
|
+
onLine,
|
|
1458
|
+
signal,
|
|
1459
|
+
step: "402-buyer-fulfillment-ack",
|
|
1460
|
+
flow
|
|
1461
|
+
}).catch(() => void 0);
|
|
1462
|
+
return { body, payment: { tradeNo, outTradeNo: reqId }, media };
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
// src/client/alipay/router.ts
|
|
1467
|
+
init_esm_shims();
|
|
1468
|
+
var ALIPAY_RAIL = "alipay";
|
|
1469
|
+
function railOf(req) {
|
|
1470
|
+
if (req.scheme === "alipay-aipay" || req.network === ALIPAY_RAIL) return ALIPAY_RAIL;
|
|
1471
|
+
return networkToChainName(req.network) ?? req.network;
|
|
1472
|
+
}
|
|
1473
|
+
function findRail(accepts, rail) {
|
|
1474
|
+
const requirement = accepts.find((r) => railOf(r) === rail);
|
|
1475
|
+
return requirement ? { rail, requirement } : null;
|
|
1476
|
+
}
|
|
1477
|
+
function selectRail(input) {
|
|
1478
|
+
const { serverAccepts, explicitRail, preference, availability } = input;
|
|
1479
|
+
if (!serverAccepts || serverAccepts.length === 0) {
|
|
1480
|
+
throw new UnsupportedRailError(explicitRail ?? "unknown", "Server offered no payment options");
|
|
1481
|
+
}
|
|
1482
|
+
if (explicitRail) {
|
|
1483
|
+
const hit = findRail(serverAccepts, explicitRail);
|
|
1484
|
+
if (!hit) {
|
|
1485
|
+
const offered = serverAccepts.map(railOf);
|
|
1486
|
+
throw new UnsupportedRailError(
|
|
1487
|
+
explicitRail,
|
|
1488
|
+
`Server doesn't accept rail '${explicitRail}'. Offered: ${offered.join(", ")}`
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
return hit;
|
|
1492
|
+
}
|
|
1493
|
+
if (preference) {
|
|
1494
|
+
for (const pref of preference) {
|
|
1495
|
+
const hit = findRail(serverAccepts, pref);
|
|
1496
|
+
if (hit) return hit;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (availability?.evmReady) {
|
|
1500
|
+
const evm = serverAccepts.find((r) => railOf(r) !== ALIPAY_RAIL);
|
|
1501
|
+
if (evm) return { rail: railOf(evm), requirement: evm };
|
|
1502
|
+
}
|
|
1503
|
+
if (availability?.alipayReady) {
|
|
1504
|
+
const hit = findRail(serverAccepts, ALIPAY_RAIL);
|
|
1505
|
+
if (hit) return hit;
|
|
1506
|
+
}
|
|
1507
|
+
return { rail: railOf(serverAccepts[0]), requirement: serverAccepts[0] };
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/client/types.ts
|
|
1511
|
+
init_esm_shims();
|
|
1512
|
+
|
|
1513
|
+
// src/client/node/index.ts
|
|
585
1514
|
var DEFAULT_CONFIG = {
|
|
586
1515
|
chain: "base",
|
|
587
1516
|
limits: {
|
|
@@ -594,15 +1523,24 @@ var MoltsPayClient = class {
|
|
|
594
1523
|
config;
|
|
595
1524
|
walletData = null;
|
|
596
1525
|
wallet = null;
|
|
1526
|
+
signer = null;
|
|
597
1527
|
todaySpending = 0;
|
|
598
1528
|
lastSpendingReset = 0;
|
|
1529
|
+
railPreference;
|
|
1530
|
+
alipaySessionId;
|
|
599
1531
|
constructor(options = {}) {
|
|
600
|
-
this.configDir = options.configDir ||
|
|
1532
|
+
this.configDir = options.configDir || join3(homedir3(), ".moltspay");
|
|
601
1533
|
this.config = this.loadConfig();
|
|
1534
|
+
this.railPreference = options.railPreference ?? this.config.railPreference;
|
|
1535
|
+
this.alipaySessionId = options.alipaySessionId;
|
|
602
1536
|
this.walletData = this.loadWallet();
|
|
603
1537
|
this.loadSpending();
|
|
604
1538
|
if (this.walletData) {
|
|
605
|
-
this.wallet = new
|
|
1539
|
+
this.wallet = new Wallet2(this.walletData.privateKey);
|
|
1540
|
+
const configDir = this.configDir;
|
|
1541
|
+
this.signer = new NodeSigner(this.wallet, {
|
|
1542
|
+
getSolanaKeypair: () => loadSolanaWallet(configDir)
|
|
1543
|
+
});
|
|
606
1544
|
}
|
|
607
1545
|
}
|
|
608
1546
|
/**
|
|
@@ -672,6 +1610,9 @@ var MoltsPayClient = class {
|
|
|
672
1610
|
* @param options - Payment options (token selection)
|
|
673
1611
|
*/
|
|
674
1612
|
async pay(serverUrl, service, params, options = {}) {
|
|
1613
|
+
if (options.rail === ALIPAY_RAIL) {
|
|
1614
|
+
return this.payViaAlipay(serverUrl, service, params, options);
|
|
1615
|
+
}
|
|
675
1616
|
if (!this.wallet || !this.walletData) {
|
|
676
1617
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
677
1618
|
}
|
|
@@ -730,20 +1671,6 @@ var MoltsPayClient = class {
|
|
|
730
1671
|
} catch {
|
|
731
1672
|
throw new Error("Invalid x-payment-required header");
|
|
732
1673
|
}
|
|
733
|
-
const networkToChainName = (network2) => {
|
|
734
|
-
if (network2 === "solana:mainnet") return "solana";
|
|
735
|
-
if (network2 === "solana:devnet") return "solana_devnet";
|
|
736
|
-
const match = network2.match(/^eip155:(\d+)$/);
|
|
737
|
-
if (!match) return null;
|
|
738
|
-
const chainId = parseInt(match[1]);
|
|
739
|
-
if (chainId === 8453) return "base";
|
|
740
|
-
if (chainId === 137) return "polygon";
|
|
741
|
-
if (chainId === 84532) return "base_sepolia";
|
|
742
|
-
if (chainId === 42431) return "tempo_moderato";
|
|
743
|
-
if (chainId === 56) return "bnb";
|
|
744
|
-
if (chainId === 97) return "bnb_testnet";
|
|
745
|
-
return null;
|
|
746
|
-
};
|
|
747
1674
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
748
1675
|
const userSpecifiedChain = options.chain;
|
|
749
1676
|
let selectedChain;
|
|
@@ -877,6 +1804,76 @@ Please specify: --chain <chain_name>`
|
|
|
877
1804
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
878
1805
|
return result.result || result;
|
|
879
1806
|
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Pay for a service over the Alipay fiat rail (2.0.0).
|
|
1809
|
+
*
|
|
1810
|
+
* Unlike the crypto path this needs no EVM wallet — it shells out to
|
|
1811
|
+
* alipay-bot via {@link AlipayClient}. Flow: hit the resource with no
|
|
1812
|
+
* payment to get the 402 challenge, confirm the server actually offers the
|
|
1813
|
+
* alipay rail (selectRail), then run the 8-step state machine and return the
|
|
1814
|
+
* resource body.
|
|
1815
|
+
*/
|
|
1816
|
+
async payViaAlipay(serverUrl, service, params, options) {
|
|
1817
|
+
const flow = this.alipaySessionId;
|
|
1818
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
1819
|
+
try {
|
|
1820
|
+
const services = await timeStep(
|
|
1821
|
+
"discover-services",
|
|
1822
|
+
flow,
|
|
1823
|
+
() => this.getServices(serverUrl)
|
|
1824
|
+
);
|
|
1825
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
1826
|
+
if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
1827
|
+
} catch {
|
|
1828
|
+
}
|
|
1829
|
+
const requestBody = options.rawData ? { service, ...params } : { service, params };
|
|
1830
|
+
const bodyJson = JSON.stringify(requestBody);
|
|
1831
|
+
const res = await timeStep(
|
|
1832
|
+
"challenge-402",
|
|
1833
|
+
flow,
|
|
1834
|
+
() => fetch(executeUrl, {
|
|
1835
|
+
method: "POST",
|
|
1836
|
+
headers: { "Content-Type": "application/json" },
|
|
1837
|
+
body: bodyJson
|
|
1838
|
+
})
|
|
1839
|
+
);
|
|
1840
|
+
if (res.status !== 402) {
|
|
1841
|
+
const data = await res.json().catch(() => ({}));
|
|
1842
|
+
if (res.ok && data.result) return data.result;
|
|
1843
|
+
throw new Error(data.error || `Expected 402, got ${res.status}`);
|
|
1844
|
+
}
|
|
1845
|
+
const header = res.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1846
|
+
if (!header) throw new Error("Missing x-payment-required header on 402");
|
|
1847
|
+
const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
|
|
1848
|
+
const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
|
|
1849
|
+
const { requirement } = selectRail({
|
|
1850
|
+
serverAccepts: accepts,
|
|
1851
|
+
explicitRail: ALIPAY_RAIL,
|
|
1852
|
+
preference: this.railPreference,
|
|
1853
|
+
availability: { evmReady: this.isInitialized }
|
|
1854
|
+
});
|
|
1855
|
+
const onLine = options.onLine ?? ((line) => process.stdout.write(line + "\n"));
|
|
1856
|
+
const alipay = new AlipayClient({
|
|
1857
|
+
sessionId: this.alipaySessionId,
|
|
1858
|
+
configDir: this.configDir
|
|
1859
|
+
});
|
|
1860
|
+
const result = await alipay.pay402({
|
|
1861
|
+
resourceUrl: executeUrl,
|
|
1862
|
+
requirement,
|
|
1863
|
+
method: "POST",
|
|
1864
|
+
data: bodyJson,
|
|
1865
|
+
onLine,
|
|
1866
|
+
onPaymentPending: options.onPaymentPending,
|
|
1867
|
+
timeoutMs: options.timeoutMs,
|
|
1868
|
+
signal: options.signal
|
|
1869
|
+
});
|
|
1870
|
+
try {
|
|
1871
|
+
const json = JSON.parse(result.body);
|
|
1872
|
+
return json.result ?? json;
|
|
1873
|
+
} catch {
|
|
1874
|
+
return { body: result.body, payment: result.payment, media: result.media };
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
880
1877
|
/**
|
|
881
1878
|
* Handle MPP (Machine Payments Protocol) payment flow
|
|
882
1879
|
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
@@ -972,14 +1969,14 @@ Please specify: --chain <chain_name>`
|
|
|
972
1969
|
async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
|
|
973
1970
|
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
974
1971
|
const tokenConfig = chain.tokens[token];
|
|
975
|
-
const provider = new
|
|
1972
|
+
const provider = new ethers2.JsonRpcProvider(chain.rpc);
|
|
976
1973
|
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
977
1974
|
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
978
1975
|
if (allowance < amountWeiCheck) {
|
|
979
1976
|
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
980
|
-
const minGasBalance =
|
|
1977
|
+
const minGasBalance = ethers2.parseEther("0.0005");
|
|
981
1978
|
if (nativeBalance < minGasBalance) {
|
|
982
|
-
const nativeBNB = parseFloat(
|
|
1979
|
+
const nativeBNB = parseFloat(ethers2.formatEther(nativeBalance)).toFixed(4);
|
|
983
1980
|
const isTestnet = chainName === "bnb_testnet";
|
|
984
1981
|
if (isTestnet) {
|
|
985
1982
|
throw new Error(
|
|
@@ -1013,35 +2010,21 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1013
2010
|
);
|
|
1014
2011
|
}
|
|
1015
2012
|
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
1016
|
-
const
|
|
2013
|
+
const intentNonce = Date.now();
|
|
2014
|
+
const intentDeadline = Date.now() + 36e5;
|
|
2015
|
+
const envelope = buildBnbIntentTypedData({
|
|
1017
2016
|
from: this.wallet.address,
|
|
1018
2017
|
to,
|
|
1019
2018
|
amount: amountWei,
|
|
1020
|
-
|
|
2019
|
+
tokenAddress: tokenConfig.address,
|
|
1021
2020
|
service,
|
|
1022
|
-
nonce:
|
|
1023
|
-
|
|
1024
|
-
deadline: Date.now() + 36e5
|
|
1025
|
-
// 1 hour
|
|
1026
|
-
};
|
|
1027
|
-
const domain = {
|
|
1028
|
-
name: "MoltsPay",
|
|
1029
|
-
version: "1",
|
|
2021
|
+
nonce: intentNonce,
|
|
2022
|
+
deadline: intentDeadline,
|
|
1030
2023
|
chainId: chain.chainId
|
|
1031
|
-
};
|
|
1032
|
-
const types = {
|
|
1033
|
-
PaymentIntent: [
|
|
1034
|
-
{ name: "from", type: "address" },
|
|
1035
|
-
{ name: "to", type: "address" },
|
|
1036
|
-
{ name: "amount", type: "uint256" },
|
|
1037
|
-
{ name: "token", type: "address" },
|
|
1038
|
-
{ name: "service", type: "string" },
|
|
1039
|
-
{ name: "nonce", type: "uint256" },
|
|
1040
|
-
{ name: "deadline", type: "uint256" }
|
|
1041
|
-
]
|
|
1042
|
-
};
|
|
2024
|
+
});
|
|
1043
2025
|
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
1044
|
-
const signature = await this.
|
|
2026
|
+
const signature = await this.signer.signTypedData(envelope);
|
|
2027
|
+
const intent = envelope.message;
|
|
1045
2028
|
const network = `eip155:${chain.chainId}`;
|
|
1046
2029
|
const payload = {
|
|
1047
2030
|
x402Version: 2,
|
|
@@ -1115,12 +2098,11 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1115
2098
|
feePayerPubkey
|
|
1116
2099
|
// Optional fee payer for gasless mode
|
|
1117
2100
|
);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
2101
|
+
const unsignedBase64 = transaction.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
|
|
2102
|
+
const signedTx = await this.signer.signSolanaTransaction({
|
|
2103
|
+
transactionBase64: unsignedBase64,
|
|
2104
|
+
partialSign: !!feePayerPubkey
|
|
2105
|
+
});
|
|
1124
2106
|
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
1125
2107
|
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
1126
2108
|
const payload = {
|
|
@@ -1167,7 +2149,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1167
2149
|
* Check ERC20 allowance for a spender
|
|
1168
2150
|
*/
|
|
1169
2151
|
async checkAllowance(tokenAddress, spender, provider) {
|
|
1170
|
-
const contract = new
|
|
2152
|
+
const contract = new ethers2.Contract(
|
|
1171
2153
|
tokenAddress,
|
|
1172
2154
|
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
1173
2155
|
provider
|
|
@@ -1178,41 +2160,29 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1178
2160
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
1179
2161
|
* This only signs - no on-chain transaction, no gas needed.
|
|
1180
2162
|
* Supports both USDC and USDT.
|
|
2163
|
+
*
|
|
2164
|
+
* Delegates typed-data construction to `core/eip3009.ts` and the signature
|
|
2165
|
+
* itself to `this.signer`. That way Web Client (Phase 4) can reuse the same
|
|
2166
|
+
* flow with an EIP-1193 signer without duplicating typed-data layout.
|
|
1181
2167
|
*/
|
|
1182
2168
|
async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
|
|
1183
|
-
const validAfter = 0;
|
|
1184
|
-
const validBefore = Math.floor(Date.now() / 1e3) + 3600;
|
|
1185
|
-
const nonce = ethers.hexlify(ethers.randomBytes(32));
|
|
1186
2169
|
const tokenConfig = chain.tokens[token];
|
|
1187
2170
|
const value = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
1188
|
-
const
|
|
2171
|
+
const nonce = ethers2.hexlify(ethers2.randomBytes(32));
|
|
2172
|
+
const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
|
|
2173
|
+
const tokenVersion = domainOverride?.version || "2";
|
|
2174
|
+
const envelope = buildEIP3009TypedData({
|
|
1189
2175
|
from: this.wallet.address,
|
|
1190
2176
|
to,
|
|
1191
2177
|
value,
|
|
1192
|
-
|
|
1193
|
-
validBefore: validBefore.toString(),
|
|
1194
|
-
nonce
|
|
1195
|
-
};
|
|
1196
|
-
const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
|
|
1197
|
-
const tokenVersion = domainOverride?.version || "2";
|
|
1198
|
-
const domain = {
|
|
1199
|
-
name: tokenName,
|
|
1200
|
-
version: tokenVersion,
|
|
2178
|
+
nonce,
|
|
1201
2179
|
chainId: chain.chainId,
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
{ name: "value", type: "uint256" },
|
|
1209
|
-
{ name: "validAfter", type: "uint256" },
|
|
1210
|
-
{ name: "validBefore", type: "uint256" },
|
|
1211
|
-
{ name: "nonce", type: "bytes32" }
|
|
1212
|
-
]
|
|
1213
|
-
};
|
|
1214
|
-
const signature = await this.wallet.signTypedData(domain, types, authorization);
|
|
1215
|
-
return { authorization, signature };
|
|
2180
|
+
tokenAddress: tokenConfig.address,
|
|
2181
|
+
tokenName,
|
|
2182
|
+
tokenVersion
|
|
2183
|
+
});
|
|
2184
|
+
const signature = await this.signer.signTypedData(envelope);
|
|
2185
|
+
return { authorization: envelope.message, signature };
|
|
1216
2186
|
}
|
|
1217
2187
|
/**
|
|
1218
2188
|
* Check spending limits
|
|
@@ -1244,7 +2214,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1244
2214
|
}
|
|
1245
2215
|
// --- Config & Wallet Management ---
|
|
1246
2216
|
loadConfig() {
|
|
1247
|
-
const configPath =
|
|
2217
|
+
const configPath = join3(this.configDir, "config.json");
|
|
1248
2218
|
if (existsSync2(configPath)) {
|
|
1249
2219
|
const content = readFileSync2(configPath, "utf-8");
|
|
1250
2220
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
@@ -1253,14 +2223,14 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1253
2223
|
}
|
|
1254
2224
|
saveConfig() {
|
|
1255
2225
|
mkdirSync2(this.configDir, { recursive: true });
|
|
1256
|
-
const configPath =
|
|
2226
|
+
const configPath = join3(this.configDir, "config.json");
|
|
1257
2227
|
writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
|
|
1258
2228
|
}
|
|
1259
2229
|
/**
|
|
1260
2230
|
* Load spending data from disk
|
|
1261
2231
|
*/
|
|
1262
2232
|
loadSpending() {
|
|
1263
|
-
const spendingPath =
|
|
2233
|
+
const spendingPath = join3(this.configDir, "spending.json");
|
|
1264
2234
|
if (existsSync2(spendingPath)) {
|
|
1265
2235
|
try {
|
|
1266
2236
|
const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
|
|
@@ -1283,7 +2253,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1283
2253
|
*/
|
|
1284
2254
|
saveSpending() {
|
|
1285
2255
|
mkdirSync2(this.configDir, { recursive: true });
|
|
1286
|
-
const spendingPath =
|
|
2256
|
+
const spendingPath = join3(this.configDir, "spending.json");
|
|
1287
2257
|
const data = {
|
|
1288
2258
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
1289
2259
|
amount: this.todaySpending,
|
|
@@ -1292,7 +2262,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1292
2262
|
writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
|
|
1293
2263
|
}
|
|
1294
2264
|
loadWallet() {
|
|
1295
|
-
const walletPath =
|
|
2265
|
+
const walletPath = join3(this.configDir, "wallet.json");
|
|
1296
2266
|
if (existsSync2(walletPath)) {
|
|
1297
2267
|
if (process.platform !== "win32") {
|
|
1298
2268
|
try {
|
|
@@ -1316,13 +2286,13 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1316
2286
|
*/
|
|
1317
2287
|
static init(configDir, options) {
|
|
1318
2288
|
mkdirSync2(configDir, { recursive: true });
|
|
1319
|
-
const wallet =
|
|
2289
|
+
const wallet = Wallet2.createRandom();
|
|
1320
2290
|
const walletData = {
|
|
1321
2291
|
address: wallet.address,
|
|
1322
2292
|
privateKey: wallet.privateKey,
|
|
1323
2293
|
createdAt: Date.now()
|
|
1324
2294
|
};
|
|
1325
|
-
const walletPath =
|
|
2295
|
+
const walletPath = join3(configDir, "wallet.json");
|
|
1326
2296
|
writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
1327
2297
|
const config = {
|
|
1328
2298
|
chain: options.chain,
|
|
@@ -1331,7 +2301,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1331
2301
|
maxPerDay: options.maxPerDay
|
|
1332
2302
|
}
|
|
1333
2303
|
};
|
|
1334
|
-
const configPath =
|
|
2304
|
+
const configPath = join3(configDir, "config.json");
|
|
1335
2305
|
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
1336
2306
|
return { address: wallet.address, configDir };
|
|
1337
2307
|
}
|
|
@@ -1348,17 +2318,17 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1348
2318
|
} catch {
|
|
1349
2319
|
throw new Error(`Unknown chain: ${this.config.chain}`);
|
|
1350
2320
|
}
|
|
1351
|
-
const provider = new
|
|
2321
|
+
const provider = new ethers2.JsonRpcProvider(chain.rpc);
|
|
1352
2322
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
1353
2323
|
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
1354
2324
|
provider.getBalance(this.wallet.address),
|
|
1355
|
-
new
|
|
1356
|
-
new
|
|
2325
|
+
new ethers2.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2326
|
+
new ethers2.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1357
2327
|
]);
|
|
1358
2328
|
return {
|
|
1359
|
-
usdc: parseFloat(
|
|
1360
|
-
usdt: parseFloat(
|
|
1361
|
-
native: parseFloat(
|
|
2329
|
+
usdc: parseFloat(ethers2.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
2330
|
+
usdt: parseFloat(ethers2.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
2331
|
+
native: parseFloat(ethers2.formatEther(nativeBalance))
|
|
1362
2332
|
};
|
|
1363
2333
|
}
|
|
1364
2334
|
/**
|
|
@@ -1381,38 +2351,38 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1381
2351
|
supportedChains.map(async (chainName) => {
|
|
1382
2352
|
try {
|
|
1383
2353
|
const chain = getChain(chainName);
|
|
1384
|
-
const provider = new
|
|
2354
|
+
const provider = new ethers2.JsonRpcProvider(chain.rpc);
|
|
1385
2355
|
if (chainName === "tempo_moderato") {
|
|
1386
2356
|
const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
|
|
1387
2357
|
provider.getBalance(this.wallet.address),
|
|
1388
|
-
new
|
|
1389
|
-
new
|
|
1390
|
-
new
|
|
1391
|
-
new
|
|
2358
|
+
new ethers2.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2359
|
+
new ethers2.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2360
|
+
new ethers2.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2361
|
+
new ethers2.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1392
2362
|
]);
|
|
1393
2363
|
results[chainName] = {
|
|
1394
|
-
usdc: parseFloat(
|
|
2364
|
+
usdc: parseFloat(ethers2.formatUnits(pathUSD, 6)),
|
|
1395
2365
|
// pathUSD as default USDC
|
|
1396
|
-
usdt: parseFloat(
|
|
2366
|
+
usdt: parseFloat(ethers2.formatUnits(alphaUSD, 6)),
|
|
1397
2367
|
// alphaUSD as default USDT
|
|
1398
|
-
native: parseFloat(
|
|
2368
|
+
native: parseFloat(ethers2.formatEther(nativeBalance)),
|
|
1399
2369
|
tempo: {
|
|
1400
|
-
pathUSD: parseFloat(
|
|
1401
|
-
alphaUSD: parseFloat(
|
|
1402
|
-
betaUSD: parseFloat(
|
|
1403
|
-
thetaUSD: parseFloat(
|
|
2370
|
+
pathUSD: parseFloat(ethers2.formatUnits(pathUSD, 6)),
|
|
2371
|
+
alphaUSD: parseFloat(ethers2.formatUnits(alphaUSD, 6)),
|
|
2372
|
+
betaUSD: parseFloat(ethers2.formatUnits(betaUSD, 6)),
|
|
2373
|
+
thetaUSD: parseFloat(ethers2.formatUnits(thetaUSD, 6))
|
|
1404
2374
|
}
|
|
1405
2375
|
};
|
|
1406
2376
|
} else {
|
|
1407
2377
|
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
1408
2378
|
provider.getBalance(this.wallet.address),
|
|
1409
|
-
new
|
|
1410
|
-
new
|
|
2379
|
+
new ethers2.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2380
|
+
new ethers2.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1411
2381
|
]);
|
|
1412
2382
|
results[chainName] = {
|
|
1413
|
-
usdc: parseFloat(
|
|
1414
|
-
usdt: parseFloat(
|
|
1415
|
-
native: parseFloat(
|
|
2383
|
+
usdc: parseFloat(ethers2.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
2384
|
+
usdt: parseFloat(ethers2.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
2385
|
+
native: parseFloat(ethers2.formatEther(nativeBalance))
|
|
1416
2386
|
};
|
|
1417
2387
|
}
|
|
1418
2388
|
} catch (err) {
|
|
@@ -1770,16 +2740,40 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
1770
2740
|
|
|
1771
2741
|
// src/facilitators/tempo.ts
|
|
1772
2742
|
init_esm_shims();
|
|
2743
|
+
import { ethers as ethers3 } from "ethers";
|
|
1773
2744
|
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
2745
|
+
var TIP20_PERMIT_ABI = [
|
|
2746
|
+
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
|
2747
|
+
"function transferFrom(address from, address to, uint256 value) returns (bool)"
|
|
2748
|
+
];
|
|
1774
2749
|
var TempoFacilitator = class extends BaseFacilitator {
|
|
1775
2750
|
name = "tempo";
|
|
1776
2751
|
displayName = "Tempo Testnet";
|
|
1777
2752
|
supportedNetworks = ["eip155:42431"];
|
|
1778
2753
|
// Tempo Moderato
|
|
1779
2754
|
rpcUrl;
|
|
2755
|
+
settlerWallet = null;
|
|
1780
2756
|
constructor() {
|
|
1781
2757
|
super();
|
|
1782
2758
|
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
2759
|
+
const settlerKey = process.env.TEMPO_SETTLER_KEY;
|
|
2760
|
+
if (settlerKey) {
|
|
2761
|
+
try {
|
|
2762
|
+
const provider = new ethers3.JsonRpcProvider(this.rpcUrl);
|
|
2763
|
+
this.settlerWallet = new ethers3.Wallet(settlerKey, provider);
|
|
2764
|
+
} catch (err) {
|
|
2765
|
+
console.warn("[TempoFacilitator] Invalid TEMPO_SETTLER_KEY, permit settlement disabled:", err);
|
|
2766
|
+
this.settlerWallet = null;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Settler EOA address advertised to clients via `X-Payment-Required.extra.tempoSpender`.
|
|
2772
|
+
* Web Client uses this as the `spender` field in the signed EIP-2612 Permit.
|
|
2773
|
+
* Returns null if no TEMPO_SETTLER_KEY is configured — permit settlement unavailable.
|
|
2774
|
+
*/
|
|
2775
|
+
getSpenderAddress() {
|
|
2776
|
+
return this.settlerWallet?.address ?? null;
|
|
1783
2777
|
}
|
|
1784
2778
|
async healthCheck() {
|
|
1785
2779
|
const start = Date.now();
|
|
@@ -1804,7 +2798,45 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1804
2798
|
return { healthy: false, error: String(error) };
|
|
1805
2799
|
}
|
|
1806
2800
|
}
|
|
1807
|
-
async verify(paymentPayload, requirements) {
|
|
2801
|
+
async verify(paymentPayload, requirements) {
|
|
2802
|
+
const inner = paymentPayload.payload;
|
|
2803
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
2804
|
+
return this.verifyPermit(inner, requirements);
|
|
2805
|
+
}
|
|
2806
|
+
return this.verifyTxHash(paymentPayload, requirements);
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* Structural validation of an EIP-2612 permit payload. Does NOT submit
|
|
2810
|
+
* anything on-chain — actual submission happens in settlePermit().
|
|
2811
|
+
*/
|
|
2812
|
+
async verifyPermit(payload, requirements) {
|
|
2813
|
+
if (!this.settlerWallet) {
|
|
2814
|
+
return { valid: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
2815
|
+
}
|
|
2816
|
+
const p = payload.permit;
|
|
2817
|
+
if (!p || !p.owner || !p.spender || !p.value || !p.deadline) {
|
|
2818
|
+
return { valid: false, error: "Invalid permit payload: missing fields" };
|
|
2819
|
+
}
|
|
2820
|
+
if (p.spender.toLowerCase() !== this.settlerWallet.address.toLowerCase()) {
|
|
2821
|
+
return {
|
|
2822
|
+
valid: false,
|
|
2823
|
+
error: `Permit spender ${p.spender} does not match configured settler ${this.settlerWallet.address}`
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
const deadline = BigInt(p.deadline);
|
|
2827
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
2828
|
+
if (deadline <= now) {
|
|
2829
|
+
return { valid: false, error: "Permit deadline has expired" };
|
|
2830
|
+
}
|
|
2831
|
+
if (BigInt(p.value) < BigInt(requirements.amount || "0")) {
|
|
2832
|
+
return {
|
|
2833
|
+
valid: false,
|
|
2834
|
+
error: `Permit value ${p.value} is less than required ${requirements.amount}`
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
return { valid: true, details: { scheme: "permit", owner: p.owner } };
|
|
2838
|
+
}
|
|
2839
|
+
async verifyTxHash(paymentPayload, requirements) {
|
|
1808
2840
|
try {
|
|
1809
2841
|
const tempoPayload = paymentPayload.payload;
|
|
1810
2842
|
if (!tempoPayload?.txHash) {
|
|
@@ -1862,7 +2894,11 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1862
2894
|
}
|
|
1863
2895
|
}
|
|
1864
2896
|
async settle(paymentPayload, requirements) {
|
|
1865
|
-
const
|
|
2897
|
+
const inner = paymentPayload.payload;
|
|
2898
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
2899
|
+
return this.settlePermit(inner, requirements);
|
|
2900
|
+
}
|
|
2901
|
+
const verifyResult = await this.verifyTxHash(paymentPayload, requirements);
|
|
1866
2902
|
if (!verifyResult.valid) {
|
|
1867
2903
|
return { success: false, error: verifyResult.error };
|
|
1868
2904
|
}
|
|
@@ -1873,6 +2909,52 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1873
2909
|
status: "settled"
|
|
1874
2910
|
};
|
|
1875
2911
|
}
|
|
2912
|
+
/**
|
|
2913
|
+
* EIP-2612 permit settlement path. Submits two transactions on Tempo:
|
|
2914
|
+
* 1. pathUSD.permit(owner, spender=settler, value, deadline, v, r, s)
|
|
2915
|
+
* 2. pathUSD.transferFrom(owner, payTo, value)
|
|
2916
|
+
*
|
|
2917
|
+
* The settler EOA pays Tempo gas (via the TIP-20 `feeToken` mechanism — no
|
|
2918
|
+
* native tTEMPO required; any held TIP-20 token balance covers fees).
|
|
2919
|
+
*/
|
|
2920
|
+
async settlePermit(payload, requirements) {
|
|
2921
|
+
if (!this.settlerWallet) {
|
|
2922
|
+
return { success: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
2923
|
+
}
|
|
2924
|
+
if (!requirements.asset || !requirements.payTo) {
|
|
2925
|
+
return { success: false, error: "Missing asset or payTo in requirements" };
|
|
2926
|
+
}
|
|
2927
|
+
const verifyResult = await this.verifyPermit(payload, requirements);
|
|
2928
|
+
if (!verifyResult.valid) {
|
|
2929
|
+
return { success: false, error: verifyResult.error };
|
|
2930
|
+
}
|
|
2931
|
+
const token = new ethers3.Contract(requirements.asset, TIP20_PERMIT_ABI, this.settlerWallet);
|
|
2932
|
+
const p = payload.permit;
|
|
2933
|
+
try {
|
|
2934
|
+
const permitTx = await token.permit(
|
|
2935
|
+
p.owner,
|
|
2936
|
+
p.spender,
|
|
2937
|
+
p.value,
|
|
2938
|
+
p.deadline,
|
|
2939
|
+
p.v,
|
|
2940
|
+
p.r,
|
|
2941
|
+
p.s
|
|
2942
|
+
);
|
|
2943
|
+
await permitTx.wait();
|
|
2944
|
+
const transferTx = await token.transferFrom(p.owner, requirements.payTo, p.value);
|
|
2945
|
+
await transferTx.wait();
|
|
2946
|
+
return {
|
|
2947
|
+
success: true,
|
|
2948
|
+
transaction: transferTx.hash,
|
|
2949
|
+
status: "settled"
|
|
2950
|
+
};
|
|
2951
|
+
} catch (err) {
|
|
2952
|
+
return {
|
|
2953
|
+
success: false,
|
|
2954
|
+
error: `Tempo permit settlement failed: ${err.message}`
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
1876
2958
|
async getTransactionReceipt(txHash) {
|
|
1877
2959
|
const response = await fetch(this.rpcUrl, {
|
|
1878
2960
|
method: "POST",
|
|
@@ -2116,12 +3198,12 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
2116
3198
|
return this.spenderAddress;
|
|
2117
3199
|
}
|
|
2118
3200
|
async getServerAddress() {
|
|
2119
|
-
const { ethers:
|
|
2120
|
-
const wallet = new
|
|
3201
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
3202
|
+
const wallet = new ethers5.Wallet(this.serverPrivateKey);
|
|
2121
3203
|
return wallet.address;
|
|
2122
3204
|
}
|
|
2123
3205
|
async recoverIntentSigner(intent, chainId) {
|
|
2124
|
-
const { ethers:
|
|
3206
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
2125
3207
|
const domain = {
|
|
2126
3208
|
...EIP712_DOMAIN,
|
|
2127
3209
|
chainId
|
|
@@ -2135,7 +3217,7 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
2135
3217
|
nonce: intent.nonce,
|
|
2136
3218
|
deadline: intent.deadline
|
|
2137
3219
|
};
|
|
2138
|
-
const recoveredAddress =
|
|
3220
|
+
const recoveredAddress = ethers5.verifyTypedData(
|
|
2139
3221
|
domain,
|
|
2140
3222
|
INTENT_TYPES,
|
|
2141
3223
|
message,
|
|
@@ -2179,10 +3261,10 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
2179
3261
|
return result.result || "0x0";
|
|
2180
3262
|
}
|
|
2181
3263
|
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
2182
|
-
const { ethers:
|
|
2183
|
-
const provider = new
|
|
2184
|
-
const wallet = new
|
|
2185
|
-
const tokenContract = new
|
|
3264
|
+
const { ethers: ethers5 } = await import("ethers");
|
|
3265
|
+
const provider = new ethers5.JsonRpcProvider(rpcUrl);
|
|
3266
|
+
const wallet = new ethers5.Wallet(this.serverPrivateKey, provider);
|
|
3267
|
+
const tokenContract = new ethers5.Contract(token, [
|
|
2186
3268
|
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
2187
3269
|
], wallet);
|
|
2188
3270
|
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
@@ -2205,9 +3287,381 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
2205
3287
|
}
|
|
2206
3288
|
};
|
|
2207
3289
|
|
|
3290
|
+
// src/facilitators/alipay.ts
|
|
3291
|
+
init_esm_shims();
|
|
3292
|
+
import crypto2 from "crypto";
|
|
3293
|
+
|
|
3294
|
+
// src/facilitators/alipay/rsa2.ts
|
|
3295
|
+
init_esm_shims();
|
|
3296
|
+
import crypto from "crypto";
|
|
3297
|
+
function rsa2Sign(data, privateKeyPem) {
|
|
3298
|
+
const signer = crypto.createSign("RSA-SHA256");
|
|
3299
|
+
signer.update(data, "utf-8");
|
|
3300
|
+
signer.end();
|
|
3301
|
+
return signer.sign(privateKeyPem, "base64");
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
// src/facilitators/alipay/encoding.ts
|
|
3305
|
+
init_esm_shims();
|
|
3306
|
+
function base64url(input) {
|
|
3307
|
+
return Buffer.from(input, "utf-8").toString("base64url");
|
|
3308
|
+
}
|
|
3309
|
+
function decodeBase64UrlWithPadFix(input) {
|
|
3310
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
3311
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
3312
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
3313
|
+
}
|
|
3314
|
+
function toPem(key, kind) {
|
|
3315
|
+
const trimmed = key.trim();
|
|
3316
|
+
if (trimmed.includes("-----BEGIN")) return trimmed;
|
|
3317
|
+
const label = kind === "PRIVATE" ? "PRIVATE KEY" : "PUBLIC KEY";
|
|
3318
|
+
const body = trimmed.replace(/\s+/g, "").match(/.{1,64}/g)?.join("\n") ?? "";
|
|
3319
|
+
return `-----BEGIN ${label}-----
|
|
3320
|
+
${body}
|
|
3321
|
+
-----END ${label}-----
|
|
3322
|
+
`;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
// src/facilitators/alipay/openapi.ts
|
|
3326
|
+
init_esm_shims();
|
|
3327
|
+
function formatAlipayTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
3328
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
3329
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
3330
|
+
}
|
|
3331
|
+
function buildSigningString(params) {
|
|
3332
|
+
return Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
3333
|
+
}
|
|
3334
|
+
function responseWrapperKey(method) {
|
|
3335
|
+
return `${method.replace(/\./g, "_")}_response`;
|
|
3336
|
+
}
|
|
3337
|
+
async function alipayOpenApiCall(method, bizContent, config) {
|
|
3338
|
+
const publicParams = {
|
|
3339
|
+
app_id: config.app_id,
|
|
3340
|
+
method,
|
|
3341
|
+
format: "JSON",
|
|
3342
|
+
charset: "utf-8",
|
|
3343
|
+
sign_type: config.sign_type ?? "RSA2",
|
|
3344
|
+
timestamp: formatAlipayTimestamp(),
|
|
3345
|
+
version: "1.0",
|
|
3346
|
+
biz_content: JSON.stringify(bizContent)
|
|
3347
|
+
};
|
|
3348
|
+
const signingString = buildSigningString(publicParams);
|
|
3349
|
+
const sign = rsa2Sign(signingString, config.private_key_pem);
|
|
3350
|
+
const body = new URLSearchParams({ ...publicParams, sign }).toString();
|
|
3351
|
+
const response = await fetch(config.gateway_url, {
|
|
3352
|
+
method: "POST",
|
|
3353
|
+
headers: {
|
|
3354
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
|
|
3355
|
+
},
|
|
3356
|
+
body
|
|
3357
|
+
});
|
|
3358
|
+
if (!response.ok) {
|
|
3359
|
+
const text = await response.text().catch(() => "<unreadable>");
|
|
3360
|
+
throw new Error(
|
|
3361
|
+
`Alipay Open API HTTP ${response.status} for ${method}: ${text.slice(0, 500)}`
|
|
3362
|
+
);
|
|
3363
|
+
}
|
|
3364
|
+
const json = await response.json();
|
|
3365
|
+
const wrapperKey = responseWrapperKey(method);
|
|
3366
|
+
const business = json[wrapperKey];
|
|
3367
|
+
if (business === void 0 || business === null || typeof business !== "object") {
|
|
3368
|
+
throw new Error(
|
|
3369
|
+
`Alipay Open API response missing "${wrapperKey}" wrapper for ${method}: ${JSON.stringify(json).slice(0, 500)}`
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
return business;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
// src/facilitators/alipay.ts
|
|
3376
|
+
var ALIPAY_NETWORK = "alipay";
|
|
3377
|
+
var ALIPAY_SCHEME = "alipay-aipay";
|
|
3378
|
+
var ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
|
|
3379
|
+
var ALIPAY_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
|
|
3380
|
+
var ALIPAY_PAY_BEFORE_MS = 30 * 60 * 1e3;
|
|
3381
|
+
var ALIPAY_SIGNING_FIELDS = [
|
|
3382
|
+
"amount",
|
|
3383
|
+
"currency",
|
|
3384
|
+
"goods_name",
|
|
3385
|
+
"out_trade_no",
|
|
3386
|
+
"pay_before",
|
|
3387
|
+
"resource_id",
|
|
3388
|
+
"seller_id",
|
|
3389
|
+
"service_id"
|
|
3390
|
+
];
|
|
3391
|
+
var AlipayFacilitator = class extends BaseFacilitator {
|
|
3392
|
+
name = "alipay";
|
|
3393
|
+
displayName = "Alipay AI \u6536";
|
|
3394
|
+
supportedNetworks = [ALIPAY_NETWORK];
|
|
3395
|
+
config;
|
|
3396
|
+
constructor(config) {
|
|
3397
|
+
super();
|
|
3398
|
+
this.config = {
|
|
3399
|
+
gateway_url: ALIPAY_GATEWAY_PROD,
|
|
3400
|
+
sign_type: "RSA2",
|
|
3401
|
+
...config
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Build the 402 challenge for a service: signs the 8-field payload with
|
|
3406
|
+
* RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
|
|
3407
|
+
* `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
|
|
3408
|
+
*/
|
|
3409
|
+
async createPaymentRequirements(opts) {
|
|
3410
|
+
if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
|
|
3411
|
+
throw new Error(
|
|
3412
|
+
`AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
|
|
3413
|
+
);
|
|
3414
|
+
}
|
|
3415
|
+
const now = /* @__PURE__ */ new Date();
|
|
3416
|
+
const outTradeNo = opts.outTradeNo ?? generateOutTradeNo();
|
|
3417
|
+
const payBefore = formatPayBefore(now);
|
|
3418
|
+
const signedFields = {
|
|
3419
|
+
amount: opts.priceCny,
|
|
3420
|
+
currency: "CNY",
|
|
3421
|
+
goods_name: opts.goodsName,
|
|
3422
|
+
out_trade_no: outTradeNo,
|
|
3423
|
+
pay_before: payBefore,
|
|
3424
|
+
resource_id: opts.resourceId,
|
|
3425
|
+
seller_id: this.config.seller_id,
|
|
3426
|
+
service_id: opts.serviceId
|
|
3427
|
+
};
|
|
3428
|
+
const signingString = ALIPAY_SIGNING_FIELDS.map((k) => `${k}=${signedFields[k]}`).join("&");
|
|
3429
|
+
const seller_signature = rsa2Sign(signingString, this.config.private_key_pem);
|
|
3430
|
+
const challenge = {
|
|
3431
|
+
protocol: {
|
|
3432
|
+
out_trade_no: outTradeNo,
|
|
3433
|
+
amount: signedFields.amount,
|
|
3434
|
+
currency: signedFields.currency,
|
|
3435
|
+
resource_id: signedFields.resource_id,
|
|
3436
|
+
pay_before: payBefore,
|
|
3437
|
+
seller_signature,
|
|
3438
|
+
seller_sign_type: this.config.sign_type ?? "RSA2",
|
|
3439
|
+
seller_unique_id: this.config.seller_id
|
|
3440
|
+
},
|
|
3441
|
+
method: {
|
|
3442
|
+
seller_name: this.config.seller_name,
|
|
3443
|
+
seller_id: this.config.seller_id,
|
|
3444
|
+
seller_app_id: this.config.app_id,
|
|
3445
|
+
goods_name: signedFields.goods_name,
|
|
3446
|
+
seller_unique_id_key: "seller_id",
|
|
3447
|
+
service_id: signedFields.service_id
|
|
3448
|
+
}
|
|
3449
|
+
};
|
|
3450
|
+
const paymentNeededHeader = base64url(JSON.stringify(challenge));
|
|
3451
|
+
const x402Accepts = {
|
|
3452
|
+
scheme: ALIPAY_SCHEME,
|
|
3453
|
+
network: ALIPAY_NETWORK,
|
|
3454
|
+
asset: "CNY",
|
|
3455
|
+
amount: opts.priceCny,
|
|
3456
|
+
payTo: this.config.seller_id,
|
|
3457
|
+
maxTimeoutSeconds: ALIPAY_PAY_BEFORE_MS / 1e3,
|
|
3458
|
+
extra: {
|
|
3459
|
+
payment_needed_header: paymentNeededHeader,
|
|
3460
|
+
out_trade_no: outTradeNo,
|
|
3461
|
+
pay_before: payBefore,
|
|
3462
|
+
service_id: signedFields.service_id
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3465
|
+
return { x402Accepts, paymentNeededHeader };
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* Verify a `Payment-Proof` by calling
|
|
3469
|
+
* `alipay.aipay.agent.payment.verify` against the Alipay Open API.
|
|
3470
|
+
*
|
|
3471
|
+
* The proof is conveyed via `paymentPayload.payload`, which may be:
|
|
3472
|
+
* - a raw Base64URL string (the `Payment-Proof` header value), or
|
|
3473
|
+
* - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
|
|
3474
|
+
*
|
|
3475
|
+
* All failure modes (malformed payload, network errors, Alipay
|
|
3476
|
+
* `code != 10000`) return `{ valid: false, error }`. No exception
|
|
3477
|
+
* escapes, regardless of client-supplied input.
|
|
3478
|
+
*/
|
|
3479
|
+
async verify(paymentPayload, _requirements) {
|
|
3480
|
+
try {
|
|
3481
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
3482
|
+
const decoded = decodeProof(proofHeader);
|
|
3483
|
+
const response = await alipayOpenApiCall(
|
|
3484
|
+
"alipay.aipay.agent.payment.verify",
|
|
3485
|
+
{
|
|
3486
|
+
payment_proof: decoded.protocol.payment_proof,
|
|
3487
|
+
trade_no: decoded.protocol.trade_no,
|
|
3488
|
+
client_session: decoded.method.client_session
|
|
3489
|
+
},
|
|
3490
|
+
this.getOpenApiConfig()
|
|
3491
|
+
);
|
|
3492
|
+
if (response.code !== "10000") {
|
|
3493
|
+
return {
|
|
3494
|
+
valid: false,
|
|
3495
|
+
error: `alipay verify ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
3496
|
+
details: {
|
|
3497
|
+
code: response.code,
|
|
3498
|
+
sub_code: response.sub_code,
|
|
3499
|
+
sub_msg: response.sub_msg
|
|
3500
|
+
}
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
return {
|
|
3504
|
+
valid: true,
|
|
3505
|
+
details: {
|
|
3506
|
+
trade_no: response.trade_no ?? decoded.protocol.trade_no,
|
|
3507
|
+
amount: response.amount,
|
|
3508
|
+
out_trade_no: response.out_trade_no,
|
|
3509
|
+
resource_id: response.resource_id,
|
|
3510
|
+
active: response.active
|
|
3511
|
+
}
|
|
3512
|
+
};
|
|
3513
|
+
} catch (e) {
|
|
3514
|
+
return {
|
|
3515
|
+
valid: false,
|
|
3516
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
|
|
3522
|
+
* service resource has been returned to the buyer.
|
|
3523
|
+
*
|
|
3524
|
+
* Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
|
|
3525
|
+
* "履约确认失败"), this is **fire-and-forget**: the caller (registry /
|
|
3526
|
+
* server) is expected to log fulfillment failures but NOT roll back
|
|
3527
|
+
* the already-delivered resource.
|
|
3528
|
+
*
|
|
3529
|
+
* Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
|
|
3530
|
+
* does the same; the redundant Base64URL decode is negligible).
|
|
3531
|
+
*/
|
|
3532
|
+
async settle(paymentPayload, _requirements) {
|
|
3533
|
+
try {
|
|
3534
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
3535
|
+
const decoded = decodeProof(proofHeader);
|
|
3536
|
+
const tradeNo = decoded.protocol.trade_no;
|
|
3537
|
+
const response = await alipayOpenApiCall(
|
|
3538
|
+
"alipay.aipay.agent.fulfillment.confirm",
|
|
3539
|
+
{ trade_no: tradeNo },
|
|
3540
|
+
this.getOpenApiConfig()
|
|
3541
|
+
);
|
|
3542
|
+
if (response.code !== "10000") {
|
|
3543
|
+
return {
|
|
3544
|
+
success: false,
|
|
3545
|
+
transaction: tradeNo,
|
|
3546
|
+
error: `alipay fulfillment ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
3547
|
+
status: "fulfillment_failed"
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
return {
|
|
3551
|
+
success: true,
|
|
3552
|
+
transaction: tradeNo,
|
|
3553
|
+
status: "fulfilled"
|
|
3554
|
+
};
|
|
3555
|
+
} catch (e) {
|
|
3556
|
+
return {
|
|
3557
|
+
success: false,
|
|
3558
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3559
|
+
};
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* Validate that the configured RSA keys parse and that the Open API
|
|
3564
|
+
* gateway is reachable. Does NOT make a real business API call (would
|
|
3565
|
+
* burn quota and could appear as a legitimate verify attempt in logs).
|
|
3566
|
+
*/
|
|
3567
|
+
async healthCheck() {
|
|
3568
|
+
const start = Date.now();
|
|
3569
|
+
try {
|
|
3570
|
+
crypto2.createPrivateKey(this.config.private_key_pem);
|
|
3571
|
+
} catch (e) {
|
|
3572
|
+
return {
|
|
3573
|
+
healthy: false,
|
|
3574
|
+
error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
try {
|
|
3578
|
+
crypto2.createPublicKey(this.config.alipay_public_key_pem);
|
|
3579
|
+
} catch (e) {
|
|
3580
|
+
return {
|
|
3581
|
+
healthy: false,
|
|
3582
|
+
error: `alipay_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
const gatewayUrl = this.config.gateway_url ?? ALIPAY_GATEWAY_PROD;
|
|
3586
|
+
const controller = new AbortController();
|
|
3587
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
3588
|
+
const response = await fetch(gatewayUrl, {
|
|
3589
|
+
method: "HEAD",
|
|
3590
|
+
signal: controller.signal
|
|
3591
|
+
}).catch(() => null);
|
|
3592
|
+
clearTimeout(timeout);
|
|
3593
|
+
const latencyMs = Date.now() - start;
|
|
3594
|
+
if (!response) {
|
|
3595
|
+
return { healthy: false, error: `gateway unreachable: ${gatewayUrl}`, latencyMs };
|
|
3596
|
+
}
|
|
3597
|
+
return { healthy: true, latencyMs };
|
|
3598
|
+
}
|
|
3599
|
+
/** Bundle the facilitator config into the shape openapi.ts wants. */
|
|
3600
|
+
getOpenApiConfig() {
|
|
3601
|
+
return {
|
|
3602
|
+
gateway_url: this.config.gateway_url ?? ALIPAY_GATEWAY_PROD,
|
|
3603
|
+
app_id: this.config.app_id,
|
|
3604
|
+
private_key_pem: this.config.private_key_pem,
|
|
3605
|
+
alipay_public_key_pem: this.config.alipay_public_key_pem,
|
|
3606
|
+
sign_type: this.config.sign_type
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
};
|
|
3610
|
+
function generateOutTradeNo() {
|
|
3611
|
+
return "VID" + crypto2.randomBytes(22).toString("base64url").slice(0, 29);
|
|
3612
|
+
}
|
|
3613
|
+
function formatPayBefore(now) {
|
|
3614
|
+
const expiry = new Date(now.getTime() + ALIPAY_PAY_BEFORE_MS);
|
|
3615
|
+
return expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
3616
|
+
}
|
|
3617
|
+
function extractProofHeader(payload) {
|
|
3618
|
+
if (typeof payload === "string") {
|
|
3619
|
+
if (payload.length === 0) {
|
|
3620
|
+
throw new Error("alipay Payment-Proof is empty");
|
|
3621
|
+
}
|
|
3622
|
+
return payload;
|
|
3623
|
+
}
|
|
3624
|
+
if (payload !== null && typeof payload === "object") {
|
|
3625
|
+
const obj = payload;
|
|
3626
|
+
const candidate = obj.paymentProof ?? obj.proofHeader;
|
|
3627
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
3628
|
+
return candidate;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
throw new Error(
|
|
3632
|
+
"alipay payment payload must be a Base64URL string or {paymentProof: string} / {proofHeader: string}"
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3635
|
+
function decodeProof(proofHeader) {
|
|
3636
|
+
let parsed;
|
|
3637
|
+
try {
|
|
3638
|
+
parsed = JSON.parse(decodeBase64UrlWithPadFix(proofHeader));
|
|
3639
|
+
} catch (e) {
|
|
3640
|
+
throw new Error(
|
|
3641
|
+
`failed to decode Payment-Proof: ${e instanceof Error ? e.message : String(e)}`
|
|
3642
|
+
);
|
|
3643
|
+
}
|
|
3644
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
3645
|
+
throw new Error("decoded Payment-Proof is not an object");
|
|
3646
|
+
}
|
|
3647
|
+
const obj = parsed;
|
|
3648
|
+
const protocol = obj.protocol;
|
|
3649
|
+
const method = obj.method;
|
|
3650
|
+
if (!protocol || typeof protocol.payment_proof !== "string") {
|
|
3651
|
+
throw new Error("decoded Payment-Proof missing protocol.payment_proof");
|
|
3652
|
+
}
|
|
3653
|
+
if (typeof protocol.trade_no !== "string") {
|
|
3654
|
+
throw new Error("decoded Payment-Proof missing protocol.trade_no");
|
|
3655
|
+
}
|
|
3656
|
+
if (!method || typeof method.client_session !== "string") {
|
|
3657
|
+
throw new Error("decoded Payment-Proof missing method.client_session");
|
|
3658
|
+
}
|
|
3659
|
+
return parsed;
|
|
3660
|
+
}
|
|
3661
|
+
|
|
2208
3662
|
// src/facilitators/registry.ts
|
|
2209
3663
|
init_esm_shims();
|
|
2210
|
-
import { Keypair as
|
|
3664
|
+
import { Keypair as Keypair5 } from "@solana/web3.js";
|
|
2211
3665
|
import bs582 from "bs58";
|
|
2212
3666
|
var FacilitatorRegistry = class {
|
|
2213
3667
|
factories = /* @__PURE__ */ new Map();
|
|
@@ -2223,13 +3677,14 @@ var FacilitatorRegistry = class {
|
|
|
2223
3677
|
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
2224
3678
|
if (feePayerKey) {
|
|
2225
3679
|
try {
|
|
2226
|
-
feePayerKeypair =
|
|
3680
|
+
feePayerKeypair = Keypair5.fromSecretKey(bs582.decode(feePayerKey));
|
|
2227
3681
|
} catch (e) {
|
|
2228
3682
|
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
2229
3683
|
}
|
|
2230
3684
|
}
|
|
2231
3685
|
return new SolanaFacilitator({ feePayerKeypair });
|
|
2232
3686
|
});
|
|
3687
|
+
this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
|
|
2233
3688
|
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
2234
3689
|
}
|
|
2235
3690
|
/**
|
|
@@ -2457,6 +3912,11 @@ var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
|
2457
3912
|
var MPP_AUTH_HEADER = "authorization";
|
|
2458
3913
|
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
2459
3914
|
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
3915
|
+
var ALIPAY_PAYMENT_NEEDED_HEADER = "payment-needed";
|
|
3916
|
+
var ALIPAY_PAYMENT_PROOF_HEADER = "payment-proof";
|
|
3917
|
+
function headerSafe(value) {
|
|
3918
|
+
return String(value ?? "").replace(/[^\x20-\x7E]/g, (ch) => encodeURIComponent(ch)).replace(/"/g, "%22");
|
|
3919
|
+
}
|
|
2460
3920
|
var TOKEN_ADDRESSES = {
|
|
2461
3921
|
"eip155:8453": {
|
|
2462
3922
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -2498,7 +3958,7 @@ var TOKEN_ADDRESSES = {
|
|
|
2498
3958
|
// Devnet USDC
|
|
2499
3959
|
}
|
|
2500
3960
|
};
|
|
2501
|
-
var
|
|
3961
|
+
var CHAIN_TO_NETWORK2 = {
|
|
2502
3962
|
"base": "eip155:8453",
|
|
2503
3963
|
"base_sepolia": "eip155:84532",
|
|
2504
3964
|
"polygon": "eip155:137",
|
|
@@ -2529,9 +3989,13 @@ var TOKEN_DOMAINS = {
|
|
|
2529
3989
|
USDT: { name: "(PoS) Tether USD", version: "2" }
|
|
2530
3990
|
},
|
|
2531
3991
|
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
3992
|
+
// Domain names verified against on-chain DOMAIN_SEPARATOR values on 2026-04-21.
|
|
3993
|
+
// See docs/TEMPO-WEB-SUPPORT.md Section 2 and test/server/tempo-domain.test.ts.
|
|
3994
|
+
// All 4 Tempo TIP-20 tokens (pathUSD / AlphaUSD / BetaUSD / ThetaUSD) use
|
|
3995
|
+
// the token symbol with first letter capitalized + version "1".
|
|
2532
3996
|
"eip155:42431": {
|
|
2533
|
-
USDC: { name: "
|
|
2534
|
-
USDT: { name: "
|
|
3997
|
+
USDC: { name: "PathUSD", version: "1" },
|
|
3998
|
+
USDT: { name: "AlphaUSD", version: "1" }
|
|
2535
3999
|
},
|
|
2536
4000
|
// BNB Smart Chain mainnet
|
|
2537
4001
|
"eip155:56": {
|
|
@@ -2588,6 +4052,8 @@ var MoltsPayServer = class {
|
|
|
2588
4052
|
registry;
|
|
2589
4053
|
networkId;
|
|
2590
4054
|
useMainnet;
|
|
4055
|
+
/** Alipay AI 收 facilitator instance, set when `provider.alipay` is configured (2.0.0). */
|
|
4056
|
+
alipayFacilitator = null;
|
|
2591
4057
|
constructor(servicesPath, options = {}) {
|
|
2592
4058
|
loadEnvFile2();
|
|
2593
4059
|
const content = readFileSync4(servicesPath, "utf-8");
|
|
@@ -2609,7 +4075,38 @@ var MoltsPayServer = class {
|
|
|
2609
4075
|
cdp: { useMainnet: this.useMainnet }
|
|
2610
4076
|
}
|
|
2611
4077
|
};
|
|
4078
|
+
const providerAlipay = this.manifest.provider.alipay;
|
|
4079
|
+
if (providerAlipay) {
|
|
4080
|
+
try {
|
|
4081
|
+
const baseDir = path3.dirname(servicesPath);
|
|
4082
|
+
const resolvePem = (p, kind) => toPem(readFileSync4(path3.isAbsolute(p) ? p : path3.resolve(baseDir, p), "utf-8"), kind);
|
|
4083
|
+
const alipayFacilitatorConfig = {
|
|
4084
|
+
seller_id: providerAlipay.seller_id,
|
|
4085
|
+
app_id: providerAlipay.app_id,
|
|
4086
|
+
seller_name: providerAlipay.seller_name,
|
|
4087
|
+
service_id_default: providerAlipay.service_id_default,
|
|
4088
|
+
private_key_pem: resolvePem(providerAlipay.private_key_path, "PRIVATE"),
|
|
4089
|
+
alipay_public_key_pem: resolvePem(providerAlipay.alipay_public_key_path, "PUBLIC"),
|
|
4090
|
+
gateway_url: providerAlipay.gateway_url,
|
|
4091
|
+
sign_type: providerAlipay.sign_type
|
|
4092
|
+
};
|
|
4093
|
+
facilitatorConfig.config = {
|
|
4094
|
+
...facilitatorConfig.config,
|
|
4095
|
+
alipay: alipayFacilitatorConfig
|
|
4096
|
+
};
|
|
4097
|
+
facilitatorConfig.fallback = facilitatorConfig.fallback || [];
|
|
4098
|
+
if (facilitatorConfig.primary !== "alipay" && !facilitatorConfig.fallback.includes("alipay")) {
|
|
4099
|
+
facilitatorConfig.fallback.push("alipay");
|
|
4100
|
+
}
|
|
4101
|
+
} catch (err) {
|
|
4102
|
+
throw new Error(`[MoltsPay] Alipay rail configured but key load failed: ${err.message}`);
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
2612
4105
|
this.registry = new FacilitatorRegistry(facilitatorConfig);
|
|
4106
|
+
if (providerAlipay) {
|
|
4107
|
+
this.alipayFacilitator = this.registry.get("alipay");
|
|
4108
|
+
console.log(`[MoltsPay] Alipay AI \u6536 rail enabled (seller ${providerAlipay.seller_id})`);
|
|
4109
|
+
}
|
|
2613
4110
|
const primaryFacilitator = this.registry.get(facilitatorConfig.primary);
|
|
2614
4111
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
2615
4112
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
@@ -2654,14 +4151,14 @@ var MoltsPayServer = class {
|
|
|
2654
4151
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
2655
4152
|
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
2656
4153
|
return {
|
|
2657
|
-
network:
|
|
4154
|
+
network: CHAIN_TO_NETWORK2[chainName] || "eip155:8453",
|
|
2658
4155
|
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
2659
4156
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
2660
4157
|
};
|
|
2661
4158
|
});
|
|
2662
4159
|
}
|
|
2663
4160
|
const chain = provider.chain || "base";
|
|
2664
|
-
const network =
|
|
4161
|
+
const network = CHAIN_TO_NETWORK2[chain] || this.networkId;
|
|
2665
4162
|
return [{
|
|
2666
4163
|
network,
|
|
2667
4164
|
wallet: getWalletForChain(chain),
|
|
@@ -2699,14 +4196,63 @@ var MoltsPayServer = class {
|
|
|
2699
4196
|
console.log(` GET /health - Health check (incl. facilitators)`);
|
|
2700
4197
|
});
|
|
2701
4198
|
}
|
|
4199
|
+
/**
|
|
4200
|
+
* Apply CORS response headers according to the `cors` option.
|
|
4201
|
+
*
|
|
4202
|
+
* Default (`cors` unset or `true`): `Access-Control-Allow-Origin: *`. Matches 1.5.x behavior
|
|
4203
|
+
* and works for every browser client whose origin does not need to send cookies.
|
|
4204
|
+
*
|
|
4205
|
+
* `cors: false`: emit no CORS headers. Same-origin only.
|
|
4206
|
+
* `cors: string[]`: origin allowlist — echo the origin back iff it matches.
|
|
4207
|
+
* `cors: CorsOptions`: full control (allowlist + credentials + maxAge).
|
|
4208
|
+
*
|
|
4209
|
+
* The required-for-Web response headers are always exposed when CORS is active:
|
|
4210
|
+
* `X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt`.
|
|
4211
|
+
*/
|
|
4212
|
+
applyCorsHeaders(req, res) {
|
|
4213
|
+
const cors = this.options.cors;
|
|
4214
|
+
if (cors === false) {
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
const requestOrigin = req.headers.origin ?? "*";
|
|
4218
|
+
if (cors === void 0 || cors === true) {
|
|
4219
|
+
this.writeCorsHeaders(res, "*");
|
|
4220
|
+
return;
|
|
4221
|
+
}
|
|
4222
|
+
if (Array.isArray(cors)) {
|
|
4223
|
+
if (cors.includes(requestOrigin)) {
|
|
4224
|
+
this.writeCorsHeaders(res, requestOrigin);
|
|
4225
|
+
res.setHeader("Vary", "Origin");
|
|
4226
|
+
}
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
const opt = cors;
|
|
4230
|
+
const isAllowed = typeof opt.origins === "function" ? opt.origins(requestOrigin) : opt.origins.includes(requestOrigin);
|
|
4231
|
+
if (!isAllowed) {
|
|
4232
|
+
return;
|
|
4233
|
+
}
|
|
4234
|
+
this.writeCorsHeaders(res, requestOrigin);
|
|
4235
|
+
res.setHeader("Vary", "Origin");
|
|
4236
|
+
if (opt.credentials) {
|
|
4237
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
4238
|
+
}
|
|
4239
|
+
const maxAge = opt.maxAge ?? 600;
|
|
4240
|
+
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
4241
|
+
}
|
|
4242
|
+
writeCorsHeaders(res, origin) {
|
|
4243
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
4244
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
4245
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization, Payment-Proof");
|
|
4246
|
+
res.setHeader(
|
|
4247
|
+
"Access-Control-Expose-Headers",
|
|
4248
|
+
"X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt, Payment-Needed"
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
2702
4251
|
/**
|
|
2703
4252
|
* Handle incoming request
|
|
2704
4253
|
*/
|
|
2705
4254
|
async handleRequest(req, res) {
|
|
2706
|
-
|
|
2707
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2708
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
2709
|
-
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
|
|
4255
|
+
this.applyCorsHeaders(req, res);
|
|
2710
4256
|
if (req.method === "OPTIONS") {
|
|
2711
4257
|
res.writeHead(204);
|
|
2712
4258
|
res.end();
|
|
@@ -2726,7 +4272,8 @@ var MoltsPayServer = class {
|
|
|
2726
4272
|
if (url.pathname === "/execute" && req.method === "POST") {
|
|
2727
4273
|
const body = await this.readBody(req);
|
|
2728
4274
|
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
2729
|
-
|
|
4275
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
4276
|
+
return await this.handleExecute(body, paymentHeader, res, proofHeader);
|
|
2730
4277
|
}
|
|
2731
4278
|
if (url.pathname === "/proxy" && req.method === "POST") {
|
|
2732
4279
|
const clientIP = req.headers["x-real-ip"] || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "";
|
|
@@ -2744,7 +4291,8 @@ var MoltsPayServer = class {
|
|
|
2744
4291
|
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
2745
4292
|
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
2746
4293
|
const x402Header = req.headers[PAYMENT_HEADER2];
|
|
2747
|
-
|
|
4294
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
4295
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader);
|
|
2748
4296
|
}
|
|
2749
4297
|
this.sendJson(res, 404, { error: "Not found" });
|
|
2750
4298
|
} catch (err) {
|
|
@@ -2841,7 +4389,7 @@ var MoltsPayServer = class {
|
|
|
2841
4389
|
/**
|
|
2842
4390
|
* POST /execute - Execute service with x402 payment
|
|
2843
4391
|
*/
|
|
2844
|
-
async handleExecute(body, paymentHeader, res) {
|
|
4392
|
+
async handleExecute(body, paymentHeader, res, proofHeader) {
|
|
2845
4393
|
const { service, params } = body;
|
|
2846
4394
|
if (!service) {
|
|
2847
4395
|
return this.sendJson(res, 400, { error: "Missing service" });
|
|
@@ -2855,6 +4403,15 @@ var MoltsPayServer = class {
|
|
|
2855
4403
|
return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
|
|
2856
4404
|
}
|
|
2857
4405
|
}
|
|
4406
|
+
if (proofHeader) {
|
|
4407
|
+
const alipayPayment = {
|
|
4408
|
+
x402Version: X402_VERSION3,
|
|
4409
|
+
scheme: ALIPAY_SCHEME,
|
|
4410
|
+
network: ALIPAY_NETWORK,
|
|
4411
|
+
payload: proofHeader
|
|
4412
|
+
};
|
|
4413
|
+
return this.handleAlipayExecute(skill, params || {}, alipayPayment, res);
|
|
4414
|
+
}
|
|
2858
4415
|
if (!paymentHeader) {
|
|
2859
4416
|
return this.sendPaymentRequired(skill.config, res);
|
|
2860
4417
|
}
|
|
@@ -2865,6 +4422,11 @@ var MoltsPayServer = class {
|
|
|
2865
4422
|
} catch {
|
|
2866
4423
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
2867
4424
|
}
|
|
4425
|
+
const payScheme = payment.accepted?.scheme || payment.scheme;
|
|
4426
|
+
const payNetwork = payment.accepted?.network || payment.network;
|
|
4427
|
+
if (payScheme === ALIPAY_SCHEME || (payNetwork ? isAlipayChainId(payNetwork) : false)) {
|
|
4428
|
+
return this.handleAlipayExecute(skill, params || {}, payment, res);
|
|
4429
|
+
}
|
|
2868
4430
|
const validation = this.validatePayment(payment, skill.config);
|
|
2869
4431
|
if (!validation.valid) {
|
|
2870
4432
|
return this.sendJson(res, 402, { error: validation.error });
|
|
@@ -2929,6 +4491,14 @@ var MoltsPayServer = class {
|
|
|
2929
4491
|
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2930
4492
|
} catch (err) {
|
|
2931
4493
|
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
4494
|
+
settlement = { success: false, error: err.message, facilitator: "none" };
|
|
4495
|
+
}
|
|
4496
|
+
if (!settlement?.success) {
|
|
4497
|
+
return this.sendJson(res, 402, {
|
|
4498
|
+
error: "Payment settlement failed",
|
|
4499
|
+
message: settlement?.error || "Settlement returned no success state",
|
|
4500
|
+
facilitator: settlement?.facilitator
|
|
4501
|
+
});
|
|
2932
4502
|
}
|
|
2933
4503
|
}
|
|
2934
4504
|
const responseHeaders = {};
|
|
@@ -2949,13 +4519,111 @@ var MoltsPayServer = class {
|
|
|
2949
4519
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
2950
4520
|
}, responseHeaders);
|
|
2951
4521
|
}
|
|
4522
|
+
/**
|
|
4523
|
+
* Execute a service paid via the Alipay AI 收 fiat rail (2.0.0).
|
|
4524
|
+
*
|
|
4525
|
+
* Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
|
|
4526
|
+
* validation. Verify hits the Alipay Open API (`payment.verify`). Settlement
|
|
4527
|
+
* (`fulfillment.confirm`) is FIRE-AND-FORGET per ALIPAY-INTEGRATION-DESIGN
|
|
4528
|
+
* §5.1: a confirm failure is logged but does NOT fail the already-delivered
|
|
4529
|
+
* response (the buyer's payment proof was already verified).
|
|
4530
|
+
*/
|
|
4531
|
+
async handleAlipayExecute(skill, params, payment, res) {
|
|
4532
|
+
if (!this.alipayFacilitator) {
|
|
4533
|
+
return this.sendJson(res, 402, { error: "Alipay rail not configured on this server" });
|
|
4534
|
+
}
|
|
4535
|
+
const requirements = {
|
|
4536
|
+
scheme: ALIPAY_SCHEME,
|
|
4537
|
+
network: ALIPAY_NETWORK,
|
|
4538
|
+
asset: "CNY",
|
|
4539
|
+
amount: skill.config.alipay?.price_cny || "0",
|
|
4540
|
+
payTo: this.manifest.provider.alipay?.seller_id || "",
|
|
4541
|
+
maxTimeoutSeconds: 1800
|
|
4542
|
+
};
|
|
4543
|
+
console.log(`[MoltsPay] Verifying Alipay payment...`);
|
|
4544
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
4545
|
+
if (!verifyResult.valid) {
|
|
4546
|
+
return this.sendJson(res, 402, {
|
|
4547
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
4548
|
+
facilitator: verifyResult.facilitator
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
4551
|
+
console.log(`[MoltsPay] Alipay payment verified by ${verifyResult.facilitator}`);
|
|
4552
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
4553
|
+
console.log(`[MoltsPay] Executing skill: ${skill.id} (timeout: ${timeoutSeconds}s)`);
|
|
4554
|
+
let result;
|
|
4555
|
+
try {
|
|
4556
|
+
result = await Promise.race([
|
|
4557
|
+
skill.handler(params),
|
|
4558
|
+
new Promise(
|
|
4559
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
4560
|
+
)
|
|
4561
|
+
]);
|
|
4562
|
+
} catch (err) {
|
|
4563
|
+
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
4564
|
+
return this.sendJson(res, 500, {
|
|
4565
|
+
error: "Service execution failed",
|
|
4566
|
+
message: err.message
|
|
4567
|
+
});
|
|
4568
|
+
}
|
|
4569
|
+
let settlement;
|
|
4570
|
+
try {
|
|
4571
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
4572
|
+
if (settlement.success) {
|
|
4573
|
+
console.log(`[MoltsPay] Alipay fulfillment confirmed: ${settlement.transaction}`);
|
|
4574
|
+
} else {
|
|
4575
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm failed (non-fatal): ${settlement.error}`);
|
|
4576
|
+
}
|
|
4577
|
+
} catch (err) {
|
|
4578
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm threw (non-fatal): ${err.message}`);
|
|
4579
|
+
settlement = { success: false, error: err.message, facilitator: "alipay" };
|
|
4580
|
+
}
|
|
4581
|
+
const responseHeaders = {};
|
|
4582
|
+
if (settlement.success) {
|
|
4583
|
+
responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(JSON.stringify({
|
|
4584
|
+
success: true,
|
|
4585
|
+
transaction: settlement.transaction,
|
|
4586
|
+
network: ALIPAY_NETWORK,
|
|
4587
|
+
facilitator: settlement.facilitator
|
|
4588
|
+
})).toString("base64");
|
|
4589
|
+
}
|
|
4590
|
+
this.sendJson(res, 200, {
|
|
4591
|
+
success: true,
|
|
4592
|
+
result,
|
|
4593
|
+
payment: settlement.success ? { transaction: settlement.transaction, status: "fulfilled", facilitator: settlement.facilitator } : { status: "delivered_unconfirmed", error: settlement.error }
|
|
4594
|
+
}, responseHeaders);
|
|
4595
|
+
}
|
|
4596
|
+
/**
|
|
4597
|
+
* Build the Alipay 402 challenge for a service, or null when the alipay rail
|
|
4598
|
+
* isn't configured for this server or this service. Returns the x402
|
|
4599
|
+
* `accepts[]` entry plus the Base64URL `Payment-Needed` header value so the
|
|
4600
|
+
* 402 responders can dual-emit both the x402 and legacy alipay-bot formats.
|
|
4601
|
+
*/
|
|
4602
|
+
async buildAlipayChallenge(config) {
|
|
4603
|
+
if (!this.alipayFacilitator || !config.alipay) return null;
|
|
4604
|
+
try {
|
|
4605
|
+
const req = await this.alipayFacilitator.createPaymentRequirements({
|
|
4606
|
+
serviceId: config.alipay.service_id || this.manifest.provider.alipay.service_id_default,
|
|
4607
|
+
priceCny: config.alipay.price_cny,
|
|
4608
|
+
goodsName: config.alipay.goods_name,
|
|
4609
|
+
resourceId: `/execute?service=${config.id}`
|
|
4610
|
+
});
|
|
4611
|
+
return { accepts: req.x402Accepts, paymentNeededHeader: req.paymentNeededHeader };
|
|
4612
|
+
} catch (err) {
|
|
4613
|
+
console.error(`[MoltsPay] Alipay challenge build failed for ${config.id}: ${err.message}`);
|
|
4614
|
+
return null;
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
2952
4617
|
/**
|
|
2953
4618
|
* Handle MPP (Machine Payments Protocol) request
|
|
2954
4619
|
* Supports both x402 and MPP protocols on service endpoints
|
|
2955
4620
|
*/
|
|
2956
|
-
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
4621
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader) {
|
|
2957
4622
|
const config = skill.config;
|
|
2958
4623
|
const params = body || {};
|
|
4624
|
+
if (proofHeader) {
|
|
4625
|
+
return await this.handleExecute({ service: config.id, params }, void 0, res, proofHeader);
|
|
4626
|
+
}
|
|
2959
4627
|
if (x402Header) {
|
|
2960
4628
|
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
2961
4629
|
}
|
|
@@ -3061,7 +4729,7 @@ var MoltsPayServer = class {
|
|
|
3061
4729
|
/**
|
|
3062
4730
|
* Return 402 with both x402 and MPP payment requirements
|
|
3063
4731
|
*/
|
|
3064
|
-
sendMPPPaymentRequired(config, res) {
|
|
4732
|
+
async sendMPPPaymentRequired(config, res) {
|
|
3065
4733
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
3066
4734
|
const providerChains = this.getProviderChains();
|
|
3067
4735
|
const accepts = [];
|
|
@@ -3072,6 +4740,10 @@ var MoltsPayServer = class {
|
|
|
3072
4740
|
}
|
|
3073
4741
|
}
|
|
3074
4742
|
}
|
|
4743
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
4744
|
+
if (alipayChallenge) {
|
|
4745
|
+
accepts.push(alipayChallenge.accepts);
|
|
4746
|
+
}
|
|
3075
4747
|
const x402PaymentRequired = {
|
|
3076
4748
|
x402Version: X402_VERSION3,
|
|
3077
4749
|
accepts,
|
|
@@ -3099,7 +4771,7 @@ var MoltsPayServer = class {
|
|
|
3099
4771
|
};
|
|
3100
4772
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3101
4773
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3102
|
-
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
4774
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${headerSafe(this.manifest.provider.name)}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
3103
4775
|
}
|
|
3104
4776
|
const headers = {
|
|
3105
4777
|
"Content-Type": "application/problem+json",
|
|
@@ -3108,6 +4780,9 @@ var MoltsPayServer = class {
|
|
|
3108
4780
|
if (mppWwwAuth) {
|
|
3109
4781
|
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
3110
4782
|
}
|
|
4783
|
+
if (alipayChallenge) {
|
|
4784
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
4785
|
+
}
|
|
3111
4786
|
res.writeHead(402, headers);
|
|
3112
4787
|
res.end(JSON.stringify({
|
|
3113
4788
|
type: "https://paymentauth.org/problems/payment-required",
|
|
@@ -3134,7 +4809,7 @@ var MoltsPayServer = class {
|
|
|
3134
4809
|
* Return 402 with x402 payment requirements (v2 format)
|
|
3135
4810
|
* Includes requirements for all chains and all accepted currencies
|
|
3136
4811
|
*/
|
|
3137
|
-
sendPaymentRequired(config, res) {
|
|
4812
|
+
async sendPaymentRequired(config, res) {
|
|
3138
4813
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
3139
4814
|
const providerChains = this.getProviderChains();
|
|
3140
4815
|
const accepts = [];
|
|
@@ -3145,6 +4820,10 @@ var MoltsPayServer = class {
|
|
|
3145
4820
|
}
|
|
3146
4821
|
}
|
|
3147
4822
|
}
|
|
4823
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
4824
|
+
if (alipayChallenge) {
|
|
4825
|
+
accepts.push(alipayChallenge.accepts);
|
|
4826
|
+
}
|
|
3148
4827
|
const acceptedChains = providerChains.map((c) => {
|
|
3149
4828
|
if (c.network === "eip155:8453") return "base";
|
|
3150
4829
|
if (c.network === "eip155:137") return "polygon";
|
|
@@ -3162,10 +4841,14 @@ var MoltsPayServer = class {
|
|
|
3162
4841
|
}
|
|
3163
4842
|
};
|
|
3164
4843
|
const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
3165
|
-
|
|
4844
|
+
const headers = {
|
|
3166
4845
|
"Content-Type": "application/json",
|
|
3167
4846
|
[PAYMENT_REQUIRED_HEADER2]: encoded
|
|
3168
|
-
}
|
|
4847
|
+
};
|
|
4848
|
+
if (alipayChallenge) {
|
|
4849
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
4850
|
+
}
|
|
4851
|
+
res.writeHead(402, headers);
|
|
3169
4852
|
res.end(JSON.stringify({
|
|
3170
4853
|
error: "Payment required",
|
|
3171
4854
|
message: `Service requires $${config.price} ${config.currency}`,
|
|
@@ -3183,7 +4866,7 @@ var MoltsPayServer = class {
|
|
|
3183
4866
|
}
|
|
3184
4867
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
3185
4868
|
const network = payment.accepted?.network || payment.network || this.networkId;
|
|
3186
|
-
if (scheme !== "exact") {
|
|
4869
|
+
if (scheme !== "exact" && scheme !== "permit") {
|
|
3187
4870
|
return { valid: false, error: `Unsupported scheme: ${scheme}` };
|
|
3188
4871
|
}
|
|
3189
4872
|
if (!this.isNetworkAccepted(network)) {
|
|
@@ -3205,8 +4888,10 @@ var MoltsPayServer = class {
|
|
|
3205
4888
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
3206
4889
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
3207
4890
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
4891
|
+
const isTempo = selectedNetwork === "eip155:42431";
|
|
4892
|
+
const scheme = isTempo ? "permit" : "exact";
|
|
3208
4893
|
const requirements = {
|
|
3209
|
-
scheme
|
|
4894
|
+
scheme,
|
|
3210
4895
|
network: selectedNetwork,
|
|
3211
4896
|
asset: tokenAddress,
|
|
3212
4897
|
amount: amountInUnits,
|
|
@@ -3234,6 +4919,16 @@ var MoltsPayServer = class {
|
|
|
3234
4919
|
};
|
|
3235
4920
|
}
|
|
3236
4921
|
}
|
|
4922
|
+
if (isTempo) {
|
|
4923
|
+
const tempoFacilitator = this.registry.get("tempo");
|
|
4924
|
+
const tempoSpender = tempoFacilitator?.getSpenderAddress?.();
|
|
4925
|
+
if (tempoSpender) {
|
|
4926
|
+
requirements.extra = {
|
|
4927
|
+
...requirements.extra || {},
|
|
4928
|
+
tempoSpender
|
|
4929
|
+
};
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
3237
4932
|
return requirements;
|
|
3238
4933
|
}
|
|
3239
4934
|
/**
|
|
@@ -3260,12 +4955,12 @@ var MoltsPayServer = class {
|
|
|
3260
4955
|
return accepted.includes(token);
|
|
3261
4956
|
}
|
|
3262
4957
|
async readBody(req) {
|
|
3263
|
-
return new Promise((
|
|
4958
|
+
return new Promise((resolve3, reject) => {
|
|
3264
4959
|
let body = "";
|
|
3265
4960
|
req.on("data", (chunk) => body += chunk);
|
|
3266
4961
|
req.on("end", () => {
|
|
3267
4962
|
try {
|
|
3268
|
-
|
|
4963
|
+
resolve3(body ? JSON.parse(body) : {});
|
|
3269
4964
|
} catch {
|
|
3270
4965
|
reject(new Error("Invalid JSON"));
|
|
3271
4966
|
}
|
|
@@ -3368,10 +5063,10 @@ var MoltsPayServer = class {
|
|
|
3368
5063
|
}
|
|
3369
5064
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
3370
5065
|
const network = payment.accepted?.network || payment.network;
|
|
3371
|
-
if (scheme !== "exact") {
|
|
5066
|
+
if (scheme !== "exact" && scheme !== "permit") {
|
|
3372
5067
|
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
3373
5068
|
}
|
|
3374
|
-
const expectedNetwork = chain ?
|
|
5069
|
+
const expectedNetwork = chain ? CHAIN_TO_NETWORK2[chain] || this.networkId : this.networkId;
|
|
3375
5070
|
if (network !== expectedNetwork) {
|
|
3376
5071
|
return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
|
|
3377
5072
|
}
|
|
@@ -3523,7 +5218,7 @@ var MoltsPayServer = class {
|
|
|
3523
5218
|
};
|
|
3524
5219
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3525
5220
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3526
|
-
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
5221
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
3527
5222
|
res.writeHead(402, {
|
|
3528
5223
|
"Content-Type": "application/problem+json",
|
|
3529
5224
|
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
@@ -3633,7 +5328,7 @@ var MoltsPayServer = class {
|
|
|
3633
5328
|
buildProxyPaymentRequirements(config, wallet, token, chain) {
|
|
3634
5329
|
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
3635
5330
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
3636
|
-
const networkId = chain ?
|
|
5331
|
+
const networkId = chain ? CHAIN_TO_NETWORK2[chain] || this.networkId : this.networkId;
|
|
3637
5332
|
const selectedToken = token && acceptedTokens.includes(token) ? token : acceptedTokens[0];
|
|
3638
5333
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
3639
5334
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
@@ -3693,10 +5388,10 @@ init_esm_shims();
|
|
|
3693
5388
|
async function printQRCode(url) {
|
|
3694
5389
|
const qrcodeModule = await import("qrcode-terminal");
|
|
3695
5390
|
const qrcode = qrcodeModule.default || qrcodeModule;
|
|
3696
|
-
return new Promise((
|
|
5391
|
+
return new Promise((resolve3) => {
|
|
3697
5392
|
qrcode.generate(url, { small: true }, (qr) => {
|
|
3698
5393
|
console.log(qr);
|
|
3699
|
-
|
|
5394
|
+
resolve3();
|
|
3700
5395
|
});
|
|
3701
5396
|
});
|
|
3702
5397
|
}
|
|
@@ -3708,9 +5403,9 @@ if (!globalThis.crypto) {
|
|
|
3708
5403
|
}
|
|
3709
5404
|
function getVersion() {
|
|
3710
5405
|
const locations = [
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
5406
|
+
join6(__dirname, "../../package.json"),
|
|
5407
|
+
join6(__dirname, "../package.json"),
|
|
5408
|
+
join6(process.cwd(), "node_modules/moltspay/package.json")
|
|
3714
5409
|
];
|
|
3715
5410
|
for (const loc of locations) {
|
|
3716
5411
|
try {
|
|
@@ -3731,7 +5426,7 @@ var ERC20_APPROVE_ABI = [
|
|
|
3731
5426
|
];
|
|
3732
5427
|
async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = false) {
|
|
3733
5428
|
const chainConfig = CHAINS[chain];
|
|
3734
|
-
const provider = new
|
|
5429
|
+
const provider = new ethers4.JsonRpcProvider(chainConfig.rpc);
|
|
3735
5430
|
const wallet = client.getWallet();
|
|
3736
5431
|
if (!wallet) {
|
|
3737
5432
|
console.log(" \u274C No wallet found");
|
|
@@ -3740,15 +5435,15 @@ async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = fal
|
|
|
3740
5435
|
const signer = wallet.connect(provider);
|
|
3741
5436
|
console.log(` Spender: ${spenderAddress}`);
|
|
3742
5437
|
let bnbBalance = await provider.getBalance(wallet.address);
|
|
3743
|
-
const minGasRequired =
|
|
5438
|
+
const minGasRequired = ethers4.parseEther("0.0005");
|
|
3744
5439
|
if (bnbBalance < minGasRequired) {
|
|
3745
5440
|
if (sponsorGas && BNB_SPONSOR_KEY) {
|
|
3746
5441
|
console.log(" \u23F3 Sponsoring BNB gas for approvals...");
|
|
3747
5442
|
try {
|
|
3748
|
-
const sponsorWallet = new
|
|
5443
|
+
const sponsorWallet = new ethers4.Wallet(BNB_SPONSOR_KEY, provider);
|
|
3749
5444
|
const tx = await sponsorWallet.sendTransaction({
|
|
3750
5445
|
to: wallet.address,
|
|
3751
|
-
value:
|
|
5446
|
+
value: ethers4.parseEther("0.001")
|
|
3752
5447
|
});
|
|
3753
5448
|
await tx.wait();
|
|
3754
5449
|
console.log(` \u2705 Sponsored 0.001 BNB (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
@@ -3767,7 +5462,7 @@ async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = fal
|
|
|
3767
5462
|
}
|
|
3768
5463
|
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3769
5464
|
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3770
|
-
const tokenContract = new
|
|
5465
|
+
const tokenContract = new ethers4.Contract(tokenConfig.address, ERC20_APPROVE_ABI, signer);
|
|
3771
5466
|
const allowance = await tokenContract.allowance(wallet.address, spenderAddress);
|
|
3772
5467
|
if (allowance > 0n) {
|
|
3773
5468
|
console.log(` \u2705 ${tokenSymbol}: already approved for ${spenderAddress.slice(0, 10)}...`);
|
|
@@ -3775,7 +5470,7 @@ async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = fal
|
|
|
3775
5470
|
}
|
|
3776
5471
|
console.log(` \u23F3 Approving ${tokenSymbol}...`);
|
|
3777
5472
|
try {
|
|
3778
|
-
const tx = await tokenContract.approve(spenderAddress,
|
|
5473
|
+
const tx = await tokenContract.approve(spenderAddress, ethers4.MaxUint256);
|
|
3779
5474
|
await tx.wait();
|
|
3780
5475
|
console.log(` \u2705 ${tokenSymbol}: approved (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3781
5476
|
} catch (err) {
|
|
@@ -3786,10 +5481,10 @@ async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = fal
|
|
|
3786
5481
|
}
|
|
3787
5482
|
async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2) {
|
|
3788
5483
|
const chainConfig = CHAINS[chain];
|
|
3789
|
-
const provider = new
|
|
5484
|
+
const provider = new ethers4.JsonRpcProvider(chainConfig.rpc);
|
|
3790
5485
|
let spenderAddress = null;
|
|
3791
5486
|
try {
|
|
3792
|
-
const walletPath =
|
|
5487
|
+
const walletPath = join6(configDir, "wallet.json");
|
|
3793
5488
|
const walletData = JSON.parse(readFileSync5(walletPath, "utf-8"));
|
|
3794
5489
|
spenderAddress = walletData.approvals?.[chain] || null;
|
|
3795
5490
|
} catch {
|
|
@@ -3800,15 +5495,15 @@ async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2
|
|
|
3800
5495
|
}
|
|
3801
5496
|
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3802
5497
|
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3803
|
-
const tokenContract = new
|
|
5498
|
+
const tokenContract = new ethers4.Contract(tokenConfig.address, ERC20_APPROVE_ABI, provider);
|
|
3804
5499
|
const allowance = await tokenContract.allowance(address, spenderAddress);
|
|
3805
5500
|
result[tokenSymbol.toLowerCase()] = allowance > 0n;
|
|
3806
5501
|
}
|
|
3807
5502
|
return result;
|
|
3808
5503
|
}
|
|
3809
5504
|
var program = new Command();
|
|
3810
|
-
var DEFAULT_CONFIG_DIR2 =
|
|
3811
|
-
var PID_FILE =
|
|
5505
|
+
var DEFAULT_CONFIG_DIR2 = join6(homedir4(), ".moltspay");
|
|
5506
|
+
var PID_FILE = join6(DEFAULT_CONFIG_DIR2, "server.pid");
|
|
3812
5507
|
if (!existsSync5(DEFAULT_CONFIG_DIR2)) {
|
|
3813
5508
|
mkdirSync3(DEFAULT_CONFIG_DIR2, { recursive: true });
|
|
3814
5509
|
}
|
|
@@ -3817,10 +5512,10 @@ function prompt(question) {
|
|
|
3817
5512
|
input: process.stdin,
|
|
3818
5513
|
output: process.stdout
|
|
3819
5514
|
});
|
|
3820
|
-
return new Promise((
|
|
5515
|
+
return new Promise((resolve3) => {
|
|
3821
5516
|
rl.question(question, (answer) => {
|
|
3822
5517
|
rl.close();
|
|
3823
|
-
|
|
5518
|
+
resolve3(answer.trim());
|
|
3824
5519
|
});
|
|
3825
5520
|
});
|
|
3826
5521
|
}
|
|
@@ -3848,7 +5543,7 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
3848
5543
|
console.log(`
|
|
3849
5544
|
\u2705 Solana wallet created: ${address}`);
|
|
3850
5545
|
console.log(`
|
|
3851
|
-
\u{1F4C1} Config saved to: ${
|
|
5546
|
+
\u{1F4C1} Config saved to: ${join6(options.configDir, "wallet-solana.json")}`);
|
|
3852
5547
|
console.log(`
|
|
3853
5548
|
\u26A0\uFE0F IMPORTANT: Back up your wallet file!`);
|
|
3854
5549
|
console.log(` This file contains your private key!
|
|
@@ -3863,7 +5558,7 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
3863
5558
|
return;
|
|
3864
5559
|
}
|
|
3865
5560
|
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
3866
|
-
if (existsSync5(
|
|
5561
|
+
if (existsSync5(join6(options.configDir, "wallet.json"))) {
|
|
3867
5562
|
console.log('\u26A0\uFE0F EVM wallet already initialized. Use "moltspay config" to update settings.');
|
|
3868
5563
|
console.log(` Config dir: ${options.configDir}`);
|
|
3869
5564
|
return;
|
|
@@ -3889,7 +5584,7 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
3889
5584
|
console.log(`
|
|
3890
5585
|
\u{1F4C1} Config saved to: ${result.configDir}`);
|
|
3891
5586
|
console.log(`
|
|
3892
|
-
\u26A0\uFE0F IMPORTANT: Back up ${
|
|
5587
|
+
\u26A0\uFE0F IMPORTANT: Back up ${join6(result.configDir, "wallet.json")}`);
|
|
3893
5588
|
console.log(` This file contains your private key!
|
|
3894
5589
|
`);
|
|
3895
5590
|
if (chain === "bnb" || chain === "bnb_testnet") {
|
|
@@ -4054,7 +5749,7 @@ program.command("approve").description("Approve a spender address for BNB chain
|
|
|
4054
5749
|
\u{1F510} Approving spender for ${chain}...
|
|
4055
5750
|
`);
|
|
4056
5751
|
await setupBNBApprovals(client, chain, options.spender, false);
|
|
4057
|
-
const walletPath =
|
|
5752
|
+
const walletPath = join6(options.configDir || DEFAULT_CONFIG_DIR2, "wallet.json");
|
|
4058
5753
|
try {
|
|
4059
5754
|
const walletData = JSON.parse(readFileSync5(walletPath, "utf-8"));
|
|
4060
5755
|
walletData.approvals = walletData.approvals || {};
|
|
@@ -4691,17 +6386,17 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
4691
6386
|
const handlers = /* @__PURE__ */ new Map();
|
|
4692
6387
|
let provider = null;
|
|
4693
6388
|
for (const inputPath of allPaths) {
|
|
4694
|
-
const resolvedPath =
|
|
6389
|
+
const resolvedPath = resolve2(inputPath);
|
|
4695
6390
|
let manifestPath;
|
|
4696
6391
|
let skillDir;
|
|
4697
6392
|
let isSkillDir = false;
|
|
4698
|
-
if (existsSync5(
|
|
4699
|
-
manifestPath =
|
|
6393
|
+
if (existsSync5(join6(resolvedPath, "moltspay.services.json"))) {
|
|
6394
|
+
manifestPath = join6(resolvedPath, "moltspay.services.json");
|
|
4700
6395
|
skillDir = resolvedPath;
|
|
4701
6396
|
isSkillDir = true;
|
|
4702
6397
|
} else if (existsSync5(resolvedPath) && resolvedPath.endsWith(".json")) {
|
|
4703
6398
|
manifestPath = resolvedPath;
|
|
4704
|
-
skillDir =
|
|
6399
|
+
skillDir = dirname2(resolvedPath);
|
|
4705
6400
|
} else if (existsSync5(resolvedPath)) {
|
|
4706
6401
|
console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
|
|
4707
6402
|
continue;
|
|
@@ -4718,7 +6413,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
4718
6413
|
let skillModule = null;
|
|
4719
6414
|
if (isSkillDir) {
|
|
4720
6415
|
let entryPoint = "index.js";
|
|
4721
|
-
const pkgJsonPath =
|
|
6416
|
+
const pkgJsonPath = join6(skillDir, "package.json");
|
|
4722
6417
|
if (existsSync5(pkgJsonPath)) {
|
|
4723
6418
|
try {
|
|
4724
6419
|
const pkgJson = JSON.parse(readFileSync5(pkgJsonPath, "utf-8"));
|
|
@@ -4728,7 +6423,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
4728
6423
|
} catch {
|
|
4729
6424
|
}
|
|
4730
6425
|
}
|
|
4731
|
-
const modulePath =
|
|
6426
|
+
const modulePath = join6(skillDir, entryPoint);
|
|
4732
6427
|
if (existsSync5(modulePath)) {
|
|
4733
6428
|
try {
|
|
4734
6429
|
skillModule = await import(modulePath);
|
|
@@ -4754,7 +6449,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
4754
6449
|
const workdir = skillDir;
|
|
4755
6450
|
handlers.set(service.id, async (params) => {
|
|
4756
6451
|
return new Promise((resolvePromise, reject) => {
|
|
4757
|
-
const proc =
|
|
6452
|
+
const proc = spawn2("sh", ["-c", service.command], {
|
|
4758
6453
|
cwd: workdir,
|
|
4759
6454
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4760
6455
|
});
|
|
@@ -4807,7 +6502,7 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
4807
6502
|
provider,
|
|
4808
6503
|
services: allServices
|
|
4809
6504
|
};
|
|
4810
|
-
const tempManifestPath =
|
|
6505
|
+
const tempManifestPath = join6(DEFAULT_CONFIG_DIR2, "combined-manifest.json");
|
|
4811
6506
|
writeFileSync3(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
|
|
4812
6507
|
console.log(`
|
|
4813
6508
|
\u{1F4CB} Combined manifest: ${allServices.length} services`);
|
|
@@ -4870,7 +6565,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
4870
6565
|
}
|
|
4871
6566
|
process.kill(pid, "SIGTERM");
|
|
4872
6567
|
console.log("\u2705 Sent SIGTERM to server");
|
|
4873
|
-
await new Promise((
|
|
6568
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
4874
6569
|
try {
|
|
4875
6570
|
process.kill(pid, 0);
|
|
4876
6571
|
console.log("\u26A0\uFE0F Server still running, sending SIGKILL...");
|
|
@@ -4886,9 +6581,10 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
4886
6581
|
process.exit(1);
|
|
4887
6582
|
}
|
|
4888
6583
|
});
|
|
4889
|
-
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) => {
|
|
6584
|
+
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) => {
|
|
4890
6585
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
4891
|
-
|
|
6586
|
+
const useAlipay = options.rail?.toLowerCase() === "alipay";
|
|
6587
|
+
if (!useAlipay && !client.isInitialized) {
|
|
4892
6588
|
console.error("\u274C Wallet not initialized. Run: npx moltspay init");
|
|
4893
6589
|
process.exit(1);
|
|
4894
6590
|
}
|
|
@@ -4916,7 +6612,7 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
4916
6612
|
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
4917
6613
|
params.image_url = imagePath;
|
|
4918
6614
|
} else {
|
|
4919
|
-
const filePath =
|
|
6615
|
+
const filePath = resolve2(imagePath);
|
|
4920
6616
|
if (!existsSync5(filePath)) {
|
|
4921
6617
|
console.error(`\u274C Image file not found: ${filePath}`);
|
|
4922
6618
|
process.exit(1);
|
|
@@ -4933,7 +6629,7 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
4933
6629
|
}
|
|
4934
6630
|
const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
|
|
4935
6631
|
const token = (options.token || "USDC").toUpperCase();
|
|
4936
|
-
if (token === "USDT") {
|
|
6632
|
+
if (!useAlipay && token === "USDT") {
|
|
4937
6633
|
const balance = await client.getBalance();
|
|
4938
6634
|
if (balance.native < 1e-4) {
|
|
4939
6635
|
console.log("\n\u26A0\uFE0F USDT requires a small amount of ETH for gas (~$0.01)");
|
|
@@ -4957,13 +6653,31 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
4957
6653
|
console.log(` Prompt: ${params.prompt}`);
|
|
4958
6654
|
}
|
|
4959
6655
|
if (imageDisplay) console.log(` Image: ${imageDisplay}`);
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
6656
|
+
if (useAlipay) {
|
|
6657
|
+
console.log(` Rail: alipay (CNY via alipay-bot)`);
|
|
6658
|
+
} else {
|
|
6659
|
+
console.log(` Chain: ${chain || "(auto)"}`);
|
|
6660
|
+
console.log(` Token: ${token}`);
|
|
6661
|
+
console.log(` Wallet: ${client.address}`);
|
|
6662
|
+
}
|
|
4963
6663
|
console.log("");
|
|
4964
6664
|
}
|
|
4965
6665
|
try {
|
|
4966
|
-
const result = await client.pay(server, service, params, {
|
|
6666
|
+
const result = await client.pay(server, service, params, useAlipay ? {
|
|
6667
|
+
rail: "alipay",
|
|
6668
|
+
rawData: useRawData,
|
|
6669
|
+
onPaymentPending: ({ paymentUrl, shortenUrl }) => {
|
|
6670
|
+
if (!options.json) {
|
|
6671
|
+
process.stdout.write(`
|
|
6672
|
+
\u{1F4F2} \u8BF7\u7528\u652F\u4ED8\u5B9D\u626B\u7801\u6216\u8BBF\u95EE\uFF1A${shortenUrl ?? paymentUrl}
|
|
6673
|
+
|
|
6674
|
+
`);
|
|
6675
|
+
}
|
|
6676
|
+
},
|
|
6677
|
+
onLine: (line) => {
|
|
6678
|
+
if (!options.json) process.stdout.write(line + "\n");
|
|
6679
|
+
}
|
|
6680
|
+
} : {
|
|
4967
6681
|
token,
|
|
4968
6682
|
chain,
|
|
4969
6683
|
rawData: useRawData
|
|
@@ -4984,11 +6698,34 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
4984
6698
|
process.exit(1);
|
|
4985
6699
|
}
|
|
4986
6700
|
});
|
|
6701
|
+
program.command("alipay <action> [args...]").description("Alipay wallet setup via alipay-bot: check | apply | bind").allowUnknownOption().action(async (action, args) => {
|
|
6702
|
+
const map = {
|
|
6703
|
+
check: "check-wallet",
|
|
6704
|
+
apply: "apply-wallet",
|
|
6705
|
+
bind: "bind-wallet"
|
|
6706
|
+
};
|
|
6707
|
+
const sub = map[action] ?? action;
|
|
6708
|
+
const child = spawn2("alipay-bot", [sub, ...args ?? []], {
|
|
6709
|
+
stdio: "inherit",
|
|
6710
|
+
env: filterEnv(process.env)
|
|
6711
|
+
});
|
|
6712
|
+
child.on("error", (e) => {
|
|
6713
|
+
if (e?.code === "ENOENT") {
|
|
6714
|
+
console.error(
|
|
6715
|
+
"\u274C alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
|
|
6716
|
+
);
|
|
6717
|
+
} else {
|
|
6718
|
+
console.error(`\u274C ${e.message}`);
|
|
6719
|
+
}
|
|
6720
|
+
process.exit(1);
|
|
6721
|
+
});
|
|
6722
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
6723
|
+
});
|
|
4987
6724
|
program.command("validate <path>").description("Validate a moltspay.services.json file against the schema").action(async (inputPath) => {
|
|
4988
|
-
const resolvedPath =
|
|
6725
|
+
const resolvedPath = resolve2(inputPath);
|
|
4989
6726
|
let manifestPath;
|
|
4990
|
-
if (existsSync5(
|
|
4991
|
-
manifestPath =
|
|
6727
|
+
if (existsSync5(join6(resolvedPath, "moltspay.services.json"))) {
|
|
6728
|
+
manifestPath = join6(resolvedPath, "moltspay.services.json");
|
|
4992
6729
|
} else if (resolvedPath.endsWith(".json") && existsSync5(resolvedPath)) {
|
|
4993
6730
|
manifestPath = resolvedPath;
|
|
4994
6731
|
} else {
|