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/index.mjs
CHANGED
|
@@ -236,6 +236,9 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
236
236
|
}
|
|
237
237
|
};
|
|
238
238
|
|
|
239
|
+
// src/facilitators/tempo.ts
|
|
240
|
+
import { ethers } from "ethers";
|
|
241
|
+
|
|
239
242
|
// src/chains/index.ts
|
|
240
243
|
var CHAINS = {
|
|
241
244
|
// ============ Mainnet ============
|
|
@@ -415,6 +418,10 @@ function listChains() {
|
|
|
415
418
|
function getChainById(chainId) {
|
|
416
419
|
return Object.values(CHAINS).find((c) => c.chainId === chainId);
|
|
417
420
|
}
|
|
421
|
+
var ALIPAY_CHAIN_ID = "alipay";
|
|
422
|
+
function isAlipayChainId(id) {
|
|
423
|
+
return id === ALIPAY_CHAIN_ID;
|
|
424
|
+
}
|
|
418
425
|
var ERC20_ABI = [
|
|
419
426
|
"function balanceOf(address owner) view returns (uint256)",
|
|
420
427
|
"function transfer(address to, uint256 amount) returns (bool)",
|
|
@@ -431,15 +438,38 @@ var ERC20_ABI = [
|
|
|
431
438
|
|
|
432
439
|
// src/facilitators/tempo.ts
|
|
433
440
|
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
441
|
+
var TIP20_PERMIT_ABI = [
|
|
442
|
+
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
|
443
|
+
"function transferFrom(address from, address to, uint256 value) returns (bool)"
|
|
444
|
+
];
|
|
434
445
|
var TempoFacilitator = class extends BaseFacilitator {
|
|
435
446
|
name = "tempo";
|
|
436
447
|
displayName = "Tempo Testnet";
|
|
437
448
|
supportedNetworks = ["eip155:42431"];
|
|
438
449
|
// Tempo Moderato
|
|
439
450
|
rpcUrl;
|
|
451
|
+
settlerWallet = null;
|
|
440
452
|
constructor() {
|
|
441
453
|
super();
|
|
442
454
|
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
455
|
+
const settlerKey = process.env.TEMPO_SETTLER_KEY;
|
|
456
|
+
if (settlerKey) {
|
|
457
|
+
try {
|
|
458
|
+
const provider = new ethers.JsonRpcProvider(this.rpcUrl);
|
|
459
|
+
this.settlerWallet = new ethers.Wallet(settlerKey, provider);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.warn("[TempoFacilitator] Invalid TEMPO_SETTLER_KEY, permit settlement disabled:", err);
|
|
462
|
+
this.settlerWallet = null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Settler EOA address advertised to clients via `X-Payment-Required.extra.tempoSpender`.
|
|
468
|
+
* Web Client uses this as the `spender` field in the signed EIP-2612 Permit.
|
|
469
|
+
* Returns null if no TEMPO_SETTLER_KEY is configured — permit settlement unavailable.
|
|
470
|
+
*/
|
|
471
|
+
getSpenderAddress() {
|
|
472
|
+
return this.settlerWallet?.address ?? null;
|
|
443
473
|
}
|
|
444
474
|
async healthCheck() {
|
|
445
475
|
const start = Date.now();
|
|
@@ -465,6 +495,44 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
465
495
|
}
|
|
466
496
|
}
|
|
467
497
|
async verify(paymentPayload, requirements) {
|
|
498
|
+
const inner = paymentPayload.payload;
|
|
499
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
500
|
+
return this.verifyPermit(inner, requirements);
|
|
501
|
+
}
|
|
502
|
+
return this.verifyTxHash(paymentPayload, requirements);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Structural validation of an EIP-2612 permit payload. Does NOT submit
|
|
506
|
+
* anything on-chain — actual submission happens in settlePermit().
|
|
507
|
+
*/
|
|
508
|
+
async verifyPermit(payload, requirements) {
|
|
509
|
+
if (!this.settlerWallet) {
|
|
510
|
+
return { valid: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
511
|
+
}
|
|
512
|
+
const p = payload.permit;
|
|
513
|
+
if (!p || !p.owner || !p.spender || !p.value || !p.deadline) {
|
|
514
|
+
return { valid: false, error: "Invalid permit payload: missing fields" };
|
|
515
|
+
}
|
|
516
|
+
if (p.spender.toLowerCase() !== this.settlerWallet.address.toLowerCase()) {
|
|
517
|
+
return {
|
|
518
|
+
valid: false,
|
|
519
|
+
error: `Permit spender ${p.spender} does not match configured settler ${this.settlerWallet.address}`
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const deadline = BigInt(p.deadline);
|
|
523
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
524
|
+
if (deadline <= now) {
|
|
525
|
+
return { valid: false, error: "Permit deadline has expired" };
|
|
526
|
+
}
|
|
527
|
+
if (BigInt(p.value) < BigInt(requirements.amount || "0")) {
|
|
528
|
+
return {
|
|
529
|
+
valid: false,
|
|
530
|
+
error: `Permit value ${p.value} is less than required ${requirements.amount}`
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
return { valid: true, details: { scheme: "permit", owner: p.owner } };
|
|
534
|
+
}
|
|
535
|
+
async verifyTxHash(paymentPayload, requirements) {
|
|
468
536
|
try {
|
|
469
537
|
const tempoPayload = paymentPayload.payload;
|
|
470
538
|
if (!tempoPayload?.txHash) {
|
|
@@ -522,7 +590,11 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
522
590
|
}
|
|
523
591
|
}
|
|
524
592
|
async settle(paymentPayload, requirements) {
|
|
525
|
-
const
|
|
593
|
+
const inner = paymentPayload.payload;
|
|
594
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
595
|
+
return this.settlePermit(inner, requirements);
|
|
596
|
+
}
|
|
597
|
+
const verifyResult = await this.verifyTxHash(paymentPayload, requirements);
|
|
526
598
|
if (!verifyResult.valid) {
|
|
527
599
|
return { success: false, error: verifyResult.error };
|
|
528
600
|
}
|
|
@@ -533,6 +605,52 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
533
605
|
status: "settled"
|
|
534
606
|
};
|
|
535
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* EIP-2612 permit settlement path. Submits two transactions on Tempo:
|
|
610
|
+
* 1. pathUSD.permit(owner, spender=settler, value, deadline, v, r, s)
|
|
611
|
+
* 2. pathUSD.transferFrom(owner, payTo, value)
|
|
612
|
+
*
|
|
613
|
+
* The settler EOA pays Tempo gas (via the TIP-20 `feeToken` mechanism — no
|
|
614
|
+
* native tTEMPO required; any held TIP-20 token balance covers fees).
|
|
615
|
+
*/
|
|
616
|
+
async settlePermit(payload, requirements) {
|
|
617
|
+
if (!this.settlerWallet) {
|
|
618
|
+
return { success: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
619
|
+
}
|
|
620
|
+
if (!requirements.asset || !requirements.payTo) {
|
|
621
|
+
return { success: false, error: "Missing asset or payTo in requirements" };
|
|
622
|
+
}
|
|
623
|
+
const verifyResult = await this.verifyPermit(payload, requirements);
|
|
624
|
+
if (!verifyResult.valid) {
|
|
625
|
+
return { success: false, error: verifyResult.error };
|
|
626
|
+
}
|
|
627
|
+
const token = new ethers.Contract(requirements.asset, TIP20_PERMIT_ABI, this.settlerWallet);
|
|
628
|
+
const p = payload.permit;
|
|
629
|
+
try {
|
|
630
|
+
const permitTx = await token.permit(
|
|
631
|
+
p.owner,
|
|
632
|
+
p.spender,
|
|
633
|
+
p.value,
|
|
634
|
+
p.deadline,
|
|
635
|
+
p.v,
|
|
636
|
+
p.r,
|
|
637
|
+
p.s
|
|
638
|
+
);
|
|
639
|
+
await permitTx.wait();
|
|
640
|
+
const transferTx = await token.transferFrom(p.owner, requirements.payTo, p.value);
|
|
641
|
+
await transferTx.wait();
|
|
642
|
+
return {
|
|
643
|
+
success: true,
|
|
644
|
+
transaction: transferTx.hash,
|
|
645
|
+
status: "settled"
|
|
646
|
+
};
|
|
647
|
+
} catch (err) {
|
|
648
|
+
return {
|
|
649
|
+
success: false,
|
|
650
|
+
error: `Tempo permit settlement failed: ${err.message}`
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
536
654
|
async getTransactionReceipt(txHash) {
|
|
537
655
|
const response = await fetch(this.rpcUrl, {
|
|
538
656
|
method: "POST",
|
|
@@ -775,12 +893,12 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
775
893
|
return this.spenderAddress;
|
|
776
894
|
}
|
|
777
895
|
async getServerAddress() {
|
|
778
|
-
const { ethers:
|
|
779
|
-
const wallet = new
|
|
896
|
+
const { ethers: ethers7 } = await import("ethers");
|
|
897
|
+
const wallet = new ethers7.Wallet(this.serverPrivateKey);
|
|
780
898
|
return wallet.address;
|
|
781
899
|
}
|
|
782
900
|
async recoverIntentSigner(intent, chainId) {
|
|
783
|
-
const { ethers:
|
|
901
|
+
const { ethers: ethers7 } = await import("ethers");
|
|
784
902
|
const domain = {
|
|
785
903
|
...EIP712_DOMAIN,
|
|
786
904
|
chainId
|
|
@@ -794,7 +912,7 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
794
912
|
nonce: intent.nonce,
|
|
795
913
|
deadline: intent.deadline
|
|
796
914
|
};
|
|
797
|
-
const recoveredAddress =
|
|
915
|
+
const recoveredAddress = ethers7.verifyTypedData(
|
|
798
916
|
domain,
|
|
799
917
|
INTENT_TYPES,
|
|
800
918
|
message,
|
|
@@ -838,10 +956,10 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
838
956
|
return result.result || "0x0";
|
|
839
957
|
}
|
|
840
958
|
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
841
|
-
const { ethers:
|
|
842
|
-
const provider = new
|
|
843
|
-
const wallet = new
|
|
844
|
-
const tokenContract = new
|
|
959
|
+
const { ethers: ethers7 } = await import("ethers");
|
|
960
|
+
const provider = new ethers7.JsonRpcProvider(rpcUrl);
|
|
961
|
+
const wallet = new ethers7.Wallet(this.serverPrivateKey, provider);
|
|
962
|
+
const tokenContract = new ethers7.Contract(token, [
|
|
845
963
|
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
846
964
|
], wallet);
|
|
847
965
|
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
@@ -1077,16 +1195,16 @@ var SolanaFacilitator = class extends BaseFacilitator {
|
|
|
1077
1195
|
return this.supportedNetworks.includes(network);
|
|
1078
1196
|
}
|
|
1079
1197
|
};
|
|
1080
|
-
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
1198
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey, connection) {
|
|
1081
1199
|
const chainConfig = SOLANA_CHAINS[chain];
|
|
1082
|
-
const
|
|
1200
|
+
const conn = connection ?? new Connection2(chainConfig.rpc, "confirmed");
|
|
1083
1201
|
const mint = new PublicKey2(chainConfig.tokens.USDC.mint);
|
|
1084
1202
|
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
1085
1203
|
const senderATA = await getAssociatedTokenAddress(mint, senderPubkey);
|
|
1086
1204
|
const recipientATA = await getAssociatedTokenAddress(mint, recipientPubkey);
|
|
1087
1205
|
const transaction = new Transaction();
|
|
1088
1206
|
try {
|
|
1089
|
-
await getAccount(
|
|
1207
|
+
await getAccount(conn, recipientATA);
|
|
1090
1208
|
} catch {
|
|
1091
1209
|
transaction.add(
|
|
1092
1210
|
createAssociatedTokenAccountInstruction(
|
|
@@ -1117,12 +1235,380 @@ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amo
|
|
|
1117
1235
|
// decimals
|
|
1118
1236
|
)
|
|
1119
1237
|
);
|
|
1120
|
-
const { blockhash, lastValidBlockHeight } = await
|
|
1238
|
+
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash();
|
|
1121
1239
|
transaction.recentBlockhash = blockhash;
|
|
1122
1240
|
transaction.feePayer = actualFeePayer;
|
|
1123
1241
|
return transaction;
|
|
1124
1242
|
}
|
|
1125
1243
|
|
|
1244
|
+
// src/facilitators/alipay.ts
|
|
1245
|
+
import crypto2 from "crypto";
|
|
1246
|
+
|
|
1247
|
+
// src/facilitators/alipay/rsa2.ts
|
|
1248
|
+
import crypto from "crypto";
|
|
1249
|
+
function rsa2Sign(data, privateKeyPem) {
|
|
1250
|
+
const signer = crypto.createSign("RSA-SHA256");
|
|
1251
|
+
signer.update(data, "utf-8");
|
|
1252
|
+
signer.end();
|
|
1253
|
+
return signer.sign(privateKeyPem, "base64");
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/facilitators/alipay/encoding.ts
|
|
1257
|
+
function base64url(input) {
|
|
1258
|
+
return Buffer.from(input, "utf-8").toString("base64url");
|
|
1259
|
+
}
|
|
1260
|
+
function decodeBase64UrlWithPadFix(input) {
|
|
1261
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1262
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
1263
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
1264
|
+
}
|
|
1265
|
+
function toPem(key, kind) {
|
|
1266
|
+
const trimmed = key.trim();
|
|
1267
|
+
if (trimmed.includes("-----BEGIN")) return trimmed;
|
|
1268
|
+
const label = kind === "PRIVATE" ? "PRIVATE KEY" : "PUBLIC KEY";
|
|
1269
|
+
const body = trimmed.replace(/\s+/g, "").match(/.{1,64}/g)?.join("\n") ?? "";
|
|
1270
|
+
return `-----BEGIN ${label}-----
|
|
1271
|
+
${body}
|
|
1272
|
+
-----END ${label}-----
|
|
1273
|
+
`;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/facilitators/alipay/openapi.ts
|
|
1277
|
+
function formatAlipayTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
1278
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1279
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1280
|
+
}
|
|
1281
|
+
function buildSigningString(params) {
|
|
1282
|
+
return Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
1283
|
+
}
|
|
1284
|
+
function responseWrapperKey(method) {
|
|
1285
|
+
return `${method.replace(/\./g, "_")}_response`;
|
|
1286
|
+
}
|
|
1287
|
+
async function alipayOpenApiCall(method, bizContent, config) {
|
|
1288
|
+
const publicParams = {
|
|
1289
|
+
app_id: config.app_id,
|
|
1290
|
+
method,
|
|
1291
|
+
format: "JSON",
|
|
1292
|
+
charset: "utf-8",
|
|
1293
|
+
sign_type: config.sign_type ?? "RSA2",
|
|
1294
|
+
timestamp: formatAlipayTimestamp(),
|
|
1295
|
+
version: "1.0",
|
|
1296
|
+
biz_content: JSON.stringify(bizContent)
|
|
1297
|
+
};
|
|
1298
|
+
const signingString = buildSigningString(publicParams);
|
|
1299
|
+
const sign = rsa2Sign(signingString, config.private_key_pem);
|
|
1300
|
+
const body = new URLSearchParams({ ...publicParams, sign }).toString();
|
|
1301
|
+
const response = await fetch(config.gateway_url, {
|
|
1302
|
+
method: "POST",
|
|
1303
|
+
headers: {
|
|
1304
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
|
|
1305
|
+
},
|
|
1306
|
+
body
|
|
1307
|
+
});
|
|
1308
|
+
if (!response.ok) {
|
|
1309
|
+
const text = await response.text().catch(() => "<unreadable>");
|
|
1310
|
+
throw new Error(
|
|
1311
|
+
`Alipay Open API HTTP ${response.status} for ${method}: ${text.slice(0, 500)}`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
const json = await response.json();
|
|
1315
|
+
const wrapperKey = responseWrapperKey(method);
|
|
1316
|
+
const business = json[wrapperKey];
|
|
1317
|
+
if (business === void 0 || business === null || typeof business !== "object") {
|
|
1318
|
+
throw new Error(
|
|
1319
|
+
`Alipay Open API response missing "${wrapperKey}" wrapper for ${method}: ${JSON.stringify(json).slice(0, 500)}`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
return business;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// src/facilitators/alipay.ts
|
|
1326
|
+
var ALIPAY_NETWORK = "alipay";
|
|
1327
|
+
var ALIPAY_SCHEME = "alipay-aipay";
|
|
1328
|
+
var ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
|
|
1329
|
+
var ALIPAY_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
|
|
1330
|
+
var ALIPAY_PAY_BEFORE_MS = 30 * 60 * 1e3;
|
|
1331
|
+
var ALIPAY_SIGNING_FIELDS = [
|
|
1332
|
+
"amount",
|
|
1333
|
+
"currency",
|
|
1334
|
+
"goods_name",
|
|
1335
|
+
"out_trade_no",
|
|
1336
|
+
"pay_before",
|
|
1337
|
+
"resource_id",
|
|
1338
|
+
"seller_id",
|
|
1339
|
+
"service_id"
|
|
1340
|
+
];
|
|
1341
|
+
var AlipayFacilitator = class extends BaseFacilitator {
|
|
1342
|
+
name = "alipay";
|
|
1343
|
+
displayName = "Alipay AI \u6536";
|
|
1344
|
+
supportedNetworks = [ALIPAY_NETWORK];
|
|
1345
|
+
config;
|
|
1346
|
+
constructor(config) {
|
|
1347
|
+
super();
|
|
1348
|
+
this.config = {
|
|
1349
|
+
gateway_url: ALIPAY_GATEWAY_PROD,
|
|
1350
|
+
sign_type: "RSA2",
|
|
1351
|
+
...config
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Build the 402 challenge for a service: signs the 8-field payload with
|
|
1356
|
+
* RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
|
|
1357
|
+
* `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
|
|
1358
|
+
*/
|
|
1359
|
+
async createPaymentRequirements(opts) {
|
|
1360
|
+
if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
|
|
1361
|
+
throw new Error(
|
|
1362
|
+
`AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
const now = /* @__PURE__ */ new Date();
|
|
1366
|
+
const outTradeNo = opts.outTradeNo ?? generateOutTradeNo();
|
|
1367
|
+
const payBefore = formatPayBefore(now);
|
|
1368
|
+
const signedFields = {
|
|
1369
|
+
amount: opts.priceCny,
|
|
1370
|
+
currency: "CNY",
|
|
1371
|
+
goods_name: opts.goodsName,
|
|
1372
|
+
out_trade_no: outTradeNo,
|
|
1373
|
+
pay_before: payBefore,
|
|
1374
|
+
resource_id: opts.resourceId,
|
|
1375
|
+
seller_id: this.config.seller_id,
|
|
1376
|
+
service_id: opts.serviceId
|
|
1377
|
+
};
|
|
1378
|
+
const signingString = ALIPAY_SIGNING_FIELDS.map((k) => `${k}=${signedFields[k]}`).join("&");
|
|
1379
|
+
const seller_signature = rsa2Sign(signingString, this.config.private_key_pem);
|
|
1380
|
+
const challenge = {
|
|
1381
|
+
protocol: {
|
|
1382
|
+
out_trade_no: outTradeNo,
|
|
1383
|
+
amount: signedFields.amount,
|
|
1384
|
+
currency: signedFields.currency,
|
|
1385
|
+
resource_id: signedFields.resource_id,
|
|
1386
|
+
pay_before: payBefore,
|
|
1387
|
+
seller_signature,
|
|
1388
|
+
seller_sign_type: this.config.sign_type ?? "RSA2",
|
|
1389
|
+
seller_unique_id: this.config.seller_id
|
|
1390
|
+
},
|
|
1391
|
+
method: {
|
|
1392
|
+
seller_name: this.config.seller_name,
|
|
1393
|
+
seller_id: this.config.seller_id,
|
|
1394
|
+
seller_app_id: this.config.app_id,
|
|
1395
|
+
goods_name: signedFields.goods_name,
|
|
1396
|
+
seller_unique_id_key: "seller_id",
|
|
1397
|
+
service_id: signedFields.service_id
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
const paymentNeededHeader = base64url(JSON.stringify(challenge));
|
|
1401
|
+
const x402Accepts = {
|
|
1402
|
+
scheme: ALIPAY_SCHEME,
|
|
1403
|
+
network: ALIPAY_NETWORK,
|
|
1404
|
+
asset: "CNY",
|
|
1405
|
+
amount: opts.priceCny,
|
|
1406
|
+
payTo: this.config.seller_id,
|
|
1407
|
+
maxTimeoutSeconds: ALIPAY_PAY_BEFORE_MS / 1e3,
|
|
1408
|
+
extra: {
|
|
1409
|
+
payment_needed_header: paymentNeededHeader,
|
|
1410
|
+
out_trade_no: outTradeNo,
|
|
1411
|
+
pay_before: payBefore,
|
|
1412
|
+
service_id: signedFields.service_id
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
return { x402Accepts, paymentNeededHeader };
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Verify a `Payment-Proof` by calling
|
|
1419
|
+
* `alipay.aipay.agent.payment.verify` against the Alipay Open API.
|
|
1420
|
+
*
|
|
1421
|
+
* The proof is conveyed via `paymentPayload.payload`, which may be:
|
|
1422
|
+
* - a raw Base64URL string (the `Payment-Proof` header value), or
|
|
1423
|
+
* - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
|
|
1424
|
+
*
|
|
1425
|
+
* All failure modes (malformed payload, network errors, Alipay
|
|
1426
|
+
* `code != 10000`) return `{ valid: false, error }`. No exception
|
|
1427
|
+
* escapes, regardless of client-supplied input.
|
|
1428
|
+
*/
|
|
1429
|
+
async verify(paymentPayload, _requirements) {
|
|
1430
|
+
try {
|
|
1431
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1432
|
+
const decoded = decodeProof(proofHeader);
|
|
1433
|
+
const response = await alipayOpenApiCall(
|
|
1434
|
+
"alipay.aipay.agent.payment.verify",
|
|
1435
|
+
{
|
|
1436
|
+
payment_proof: decoded.protocol.payment_proof,
|
|
1437
|
+
trade_no: decoded.protocol.trade_no,
|
|
1438
|
+
client_session: decoded.method.client_session
|
|
1439
|
+
},
|
|
1440
|
+
this.getOpenApiConfig()
|
|
1441
|
+
);
|
|
1442
|
+
if (response.code !== "10000") {
|
|
1443
|
+
return {
|
|
1444
|
+
valid: false,
|
|
1445
|
+
error: `alipay verify ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1446
|
+
details: {
|
|
1447
|
+
code: response.code,
|
|
1448
|
+
sub_code: response.sub_code,
|
|
1449
|
+
sub_msg: response.sub_msg
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
return {
|
|
1454
|
+
valid: true,
|
|
1455
|
+
details: {
|
|
1456
|
+
trade_no: response.trade_no ?? decoded.protocol.trade_no,
|
|
1457
|
+
amount: response.amount,
|
|
1458
|
+
out_trade_no: response.out_trade_no,
|
|
1459
|
+
resource_id: response.resource_id,
|
|
1460
|
+
active: response.active
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
} catch (e) {
|
|
1464
|
+
return {
|
|
1465
|
+
valid: false,
|
|
1466
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
|
|
1472
|
+
* service resource has been returned to the buyer.
|
|
1473
|
+
*
|
|
1474
|
+
* Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
|
|
1475
|
+
* "履约确认失败"), this is **fire-and-forget**: the caller (registry /
|
|
1476
|
+
* server) is expected to log fulfillment failures but NOT roll back
|
|
1477
|
+
* the already-delivered resource.
|
|
1478
|
+
*
|
|
1479
|
+
* Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
|
|
1480
|
+
* does the same; the redundant Base64URL decode is negligible).
|
|
1481
|
+
*/
|
|
1482
|
+
async settle(paymentPayload, _requirements) {
|
|
1483
|
+
try {
|
|
1484
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1485
|
+
const decoded = decodeProof(proofHeader);
|
|
1486
|
+
const tradeNo = decoded.protocol.trade_no;
|
|
1487
|
+
const response = await alipayOpenApiCall(
|
|
1488
|
+
"alipay.aipay.agent.fulfillment.confirm",
|
|
1489
|
+
{ trade_no: tradeNo },
|
|
1490
|
+
this.getOpenApiConfig()
|
|
1491
|
+
);
|
|
1492
|
+
if (response.code !== "10000") {
|
|
1493
|
+
return {
|
|
1494
|
+
success: false,
|
|
1495
|
+
transaction: tradeNo,
|
|
1496
|
+
error: `alipay fulfillment ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1497
|
+
status: "fulfillment_failed"
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
return {
|
|
1501
|
+
success: true,
|
|
1502
|
+
transaction: tradeNo,
|
|
1503
|
+
status: "fulfilled"
|
|
1504
|
+
};
|
|
1505
|
+
} catch (e) {
|
|
1506
|
+
return {
|
|
1507
|
+
success: false,
|
|
1508
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Validate that the configured RSA keys parse and that the Open API
|
|
1514
|
+
* gateway is reachable. Does NOT make a real business API call (would
|
|
1515
|
+
* burn quota and could appear as a legitimate verify attempt in logs).
|
|
1516
|
+
*/
|
|
1517
|
+
async healthCheck() {
|
|
1518
|
+
const start = Date.now();
|
|
1519
|
+
try {
|
|
1520
|
+
crypto2.createPrivateKey(this.config.private_key_pem);
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
return {
|
|
1523
|
+
healthy: false,
|
|
1524
|
+
error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
try {
|
|
1528
|
+
crypto2.createPublicKey(this.config.alipay_public_key_pem);
|
|
1529
|
+
} catch (e) {
|
|
1530
|
+
return {
|
|
1531
|
+
healthy: false,
|
|
1532
|
+
error: `alipay_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
const gatewayUrl = this.config.gateway_url ?? ALIPAY_GATEWAY_PROD;
|
|
1536
|
+
const controller = new AbortController();
|
|
1537
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
1538
|
+
const response = await fetch(gatewayUrl, {
|
|
1539
|
+
method: "HEAD",
|
|
1540
|
+
signal: controller.signal
|
|
1541
|
+
}).catch(() => null);
|
|
1542
|
+
clearTimeout(timeout);
|
|
1543
|
+
const latencyMs = Date.now() - start;
|
|
1544
|
+
if (!response) {
|
|
1545
|
+
return { healthy: false, error: `gateway unreachable: ${gatewayUrl}`, latencyMs };
|
|
1546
|
+
}
|
|
1547
|
+
return { healthy: true, latencyMs };
|
|
1548
|
+
}
|
|
1549
|
+
/** Bundle the facilitator config into the shape openapi.ts wants. */
|
|
1550
|
+
getOpenApiConfig() {
|
|
1551
|
+
return {
|
|
1552
|
+
gateway_url: this.config.gateway_url ?? ALIPAY_GATEWAY_PROD,
|
|
1553
|
+
app_id: this.config.app_id,
|
|
1554
|
+
private_key_pem: this.config.private_key_pem,
|
|
1555
|
+
alipay_public_key_pem: this.config.alipay_public_key_pem,
|
|
1556
|
+
sign_type: this.config.sign_type
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
function generateOutTradeNo() {
|
|
1561
|
+
return "VID" + crypto2.randomBytes(22).toString("base64url").slice(0, 29);
|
|
1562
|
+
}
|
|
1563
|
+
function formatPayBefore(now) {
|
|
1564
|
+
const expiry = new Date(now.getTime() + ALIPAY_PAY_BEFORE_MS);
|
|
1565
|
+
return expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1566
|
+
}
|
|
1567
|
+
function extractProofHeader(payload) {
|
|
1568
|
+
if (typeof payload === "string") {
|
|
1569
|
+
if (payload.length === 0) {
|
|
1570
|
+
throw new Error("alipay Payment-Proof is empty");
|
|
1571
|
+
}
|
|
1572
|
+
return payload;
|
|
1573
|
+
}
|
|
1574
|
+
if (payload !== null && typeof payload === "object") {
|
|
1575
|
+
const obj = payload;
|
|
1576
|
+
const candidate = obj.paymentProof ?? obj.proofHeader;
|
|
1577
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
1578
|
+
return candidate;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
throw new Error(
|
|
1582
|
+
"alipay payment payload must be a Base64URL string or {paymentProof: string} / {proofHeader: string}"
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
function decodeProof(proofHeader) {
|
|
1586
|
+
let parsed;
|
|
1587
|
+
try {
|
|
1588
|
+
parsed = JSON.parse(decodeBase64UrlWithPadFix(proofHeader));
|
|
1589
|
+
} catch (e) {
|
|
1590
|
+
throw new Error(
|
|
1591
|
+
`failed to decode Payment-Proof: ${e instanceof Error ? e.message : String(e)}`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1595
|
+
throw new Error("decoded Payment-Proof is not an object");
|
|
1596
|
+
}
|
|
1597
|
+
const obj = parsed;
|
|
1598
|
+
const protocol = obj.protocol;
|
|
1599
|
+
const method = obj.method;
|
|
1600
|
+
if (!protocol || typeof protocol.payment_proof !== "string") {
|
|
1601
|
+
throw new Error("decoded Payment-Proof missing protocol.payment_proof");
|
|
1602
|
+
}
|
|
1603
|
+
if (typeof protocol.trade_no !== "string") {
|
|
1604
|
+
throw new Error("decoded Payment-Proof missing protocol.trade_no");
|
|
1605
|
+
}
|
|
1606
|
+
if (!method || typeof method.client_session !== "string") {
|
|
1607
|
+
throw new Error("decoded Payment-Proof missing method.client_session");
|
|
1608
|
+
}
|
|
1609
|
+
return parsed;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1126
1612
|
// src/facilitators/registry.ts
|
|
1127
1613
|
import { Keypair as Keypair2 } from "@solana/web3.js";
|
|
1128
1614
|
import bs58 from "bs58";
|
|
@@ -1147,6 +1633,7 @@ var FacilitatorRegistry = class {
|
|
|
1147
1633
|
}
|
|
1148
1634
|
return new SolanaFacilitator({ feePayerKeypair });
|
|
1149
1635
|
});
|
|
1636
|
+
this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
|
|
1150
1637
|
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
1151
1638
|
}
|
|
1152
1639
|
/**
|
|
@@ -1381,6 +1868,11 @@ var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
|
1381
1868
|
var MPP_AUTH_HEADER = "authorization";
|
|
1382
1869
|
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
1383
1870
|
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
1871
|
+
var ALIPAY_PAYMENT_NEEDED_HEADER = "payment-needed";
|
|
1872
|
+
var ALIPAY_PAYMENT_PROOF_HEADER = "payment-proof";
|
|
1873
|
+
function headerSafe(value) {
|
|
1874
|
+
return String(value ?? "").replace(/[^\x20-\x7E]/g, (ch) => encodeURIComponent(ch)).replace(/"/g, "%22");
|
|
1875
|
+
}
|
|
1384
1876
|
var TOKEN_ADDRESSES = {
|
|
1385
1877
|
"eip155:8453": {
|
|
1386
1878
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -1453,9 +1945,13 @@ var TOKEN_DOMAINS = {
|
|
|
1453
1945
|
USDT: { name: "(PoS) Tether USD", version: "2" }
|
|
1454
1946
|
},
|
|
1455
1947
|
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
1948
|
+
// Domain names verified against on-chain DOMAIN_SEPARATOR values on 2026-04-21.
|
|
1949
|
+
// See docs/TEMPO-WEB-SUPPORT.md Section 2 and test/server/tempo-domain.test.ts.
|
|
1950
|
+
// All 4 Tempo TIP-20 tokens (pathUSD / AlphaUSD / BetaUSD / ThetaUSD) use
|
|
1951
|
+
// the token symbol with first letter capitalized + version "1".
|
|
1456
1952
|
"eip155:42431": {
|
|
1457
|
-
USDC: { name: "
|
|
1458
|
-
USDT: { name: "
|
|
1953
|
+
USDC: { name: "PathUSD", version: "1" },
|
|
1954
|
+
USDT: { name: "AlphaUSD", version: "1" }
|
|
1459
1955
|
},
|
|
1460
1956
|
// BNB Smart Chain mainnet
|
|
1461
1957
|
"eip155:56": {
|
|
@@ -1512,6 +2008,8 @@ var MoltsPayServer = class {
|
|
|
1512
2008
|
registry;
|
|
1513
2009
|
networkId;
|
|
1514
2010
|
useMainnet;
|
|
2011
|
+
/** Alipay AI 收 facilitator instance, set when `provider.alipay` is configured (2.0.0). */
|
|
2012
|
+
alipayFacilitator = null;
|
|
1515
2013
|
constructor(servicesPath, options = {}) {
|
|
1516
2014
|
loadEnvFile2();
|
|
1517
2015
|
const content = readFileSync2(servicesPath, "utf-8");
|
|
@@ -1533,7 +2031,38 @@ var MoltsPayServer = class {
|
|
|
1533
2031
|
cdp: { useMainnet: this.useMainnet }
|
|
1534
2032
|
}
|
|
1535
2033
|
};
|
|
2034
|
+
const providerAlipay = this.manifest.provider.alipay;
|
|
2035
|
+
if (providerAlipay) {
|
|
2036
|
+
try {
|
|
2037
|
+
const baseDir = path2.dirname(servicesPath);
|
|
2038
|
+
const resolvePem = (p, kind) => toPem(readFileSync2(path2.isAbsolute(p) ? p : path2.resolve(baseDir, p), "utf-8"), kind);
|
|
2039
|
+
const alipayFacilitatorConfig = {
|
|
2040
|
+
seller_id: providerAlipay.seller_id,
|
|
2041
|
+
app_id: providerAlipay.app_id,
|
|
2042
|
+
seller_name: providerAlipay.seller_name,
|
|
2043
|
+
service_id_default: providerAlipay.service_id_default,
|
|
2044
|
+
private_key_pem: resolvePem(providerAlipay.private_key_path, "PRIVATE"),
|
|
2045
|
+
alipay_public_key_pem: resolvePem(providerAlipay.alipay_public_key_path, "PUBLIC"),
|
|
2046
|
+
gateway_url: providerAlipay.gateway_url,
|
|
2047
|
+
sign_type: providerAlipay.sign_type
|
|
2048
|
+
};
|
|
2049
|
+
facilitatorConfig.config = {
|
|
2050
|
+
...facilitatorConfig.config,
|
|
2051
|
+
alipay: alipayFacilitatorConfig
|
|
2052
|
+
};
|
|
2053
|
+
facilitatorConfig.fallback = facilitatorConfig.fallback || [];
|
|
2054
|
+
if (facilitatorConfig.primary !== "alipay" && !facilitatorConfig.fallback.includes("alipay")) {
|
|
2055
|
+
facilitatorConfig.fallback.push("alipay");
|
|
2056
|
+
}
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
throw new Error(`[MoltsPay] Alipay rail configured but key load failed: ${err.message}`);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
1536
2061
|
this.registry = new FacilitatorRegistry(facilitatorConfig);
|
|
2062
|
+
if (providerAlipay) {
|
|
2063
|
+
this.alipayFacilitator = this.registry.get("alipay");
|
|
2064
|
+
console.log(`[MoltsPay] Alipay AI \u6536 rail enabled (seller ${providerAlipay.seller_id})`);
|
|
2065
|
+
}
|
|
1537
2066
|
const primaryFacilitator = this.registry.get(facilitatorConfig.primary);
|
|
1538
2067
|
console.log(`[MoltsPay] Loaded ${this.manifest.services.length} services from ${servicesPath}`);
|
|
1539
2068
|
console.log(`[MoltsPay] Provider: ${this.manifest.provider.name}`);
|
|
@@ -1623,14 +2152,63 @@ var MoltsPayServer = class {
|
|
|
1623
2152
|
console.log(` GET /health - Health check (incl. facilitators)`);
|
|
1624
2153
|
});
|
|
1625
2154
|
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Apply CORS response headers according to the `cors` option.
|
|
2157
|
+
*
|
|
2158
|
+
* Default (`cors` unset or `true`): `Access-Control-Allow-Origin: *`. Matches 1.5.x behavior
|
|
2159
|
+
* and works for every browser client whose origin does not need to send cookies.
|
|
2160
|
+
*
|
|
2161
|
+
* `cors: false`: emit no CORS headers. Same-origin only.
|
|
2162
|
+
* `cors: string[]`: origin allowlist — echo the origin back iff it matches.
|
|
2163
|
+
* `cors: CorsOptions`: full control (allowlist + credentials + maxAge).
|
|
2164
|
+
*
|
|
2165
|
+
* The required-for-Web response headers are always exposed when CORS is active:
|
|
2166
|
+
* `X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt`.
|
|
2167
|
+
*/
|
|
2168
|
+
applyCorsHeaders(req, res) {
|
|
2169
|
+
const cors = this.options.cors;
|
|
2170
|
+
if (cors === false) {
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
const requestOrigin = req.headers.origin ?? "*";
|
|
2174
|
+
if (cors === void 0 || cors === true) {
|
|
2175
|
+
this.writeCorsHeaders(res, "*");
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
if (Array.isArray(cors)) {
|
|
2179
|
+
if (cors.includes(requestOrigin)) {
|
|
2180
|
+
this.writeCorsHeaders(res, requestOrigin);
|
|
2181
|
+
res.setHeader("Vary", "Origin");
|
|
2182
|
+
}
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const opt = cors;
|
|
2186
|
+
const isAllowed = typeof opt.origins === "function" ? opt.origins(requestOrigin) : opt.origins.includes(requestOrigin);
|
|
2187
|
+
if (!isAllowed) {
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
this.writeCorsHeaders(res, requestOrigin);
|
|
2191
|
+
res.setHeader("Vary", "Origin");
|
|
2192
|
+
if (opt.credentials) {
|
|
2193
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
2194
|
+
}
|
|
2195
|
+
const maxAge = opt.maxAge ?? 600;
|
|
2196
|
+
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
2197
|
+
}
|
|
2198
|
+
writeCorsHeaders(res, origin) {
|
|
2199
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
2200
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2201
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization, Payment-Proof");
|
|
2202
|
+
res.setHeader(
|
|
2203
|
+
"Access-Control-Expose-Headers",
|
|
2204
|
+
"X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt, Payment-Needed"
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
1626
2207
|
/**
|
|
1627
2208
|
* Handle incoming request
|
|
1628
2209
|
*/
|
|
1629
2210
|
async handleRequest(req, res) {
|
|
1630
|
-
|
|
1631
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1632
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
1633
|
-
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
|
|
2211
|
+
this.applyCorsHeaders(req, res);
|
|
1634
2212
|
if (req.method === "OPTIONS") {
|
|
1635
2213
|
res.writeHead(204);
|
|
1636
2214
|
res.end();
|
|
@@ -1650,7 +2228,8 @@ var MoltsPayServer = class {
|
|
|
1650
2228
|
if (url.pathname === "/execute" && req.method === "POST") {
|
|
1651
2229
|
const body = await this.readBody(req);
|
|
1652
2230
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
1653
|
-
|
|
2231
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
2232
|
+
return await this.handleExecute(body, paymentHeader, res, proofHeader);
|
|
1654
2233
|
}
|
|
1655
2234
|
if (url.pathname === "/proxy" && req.method === "POST") {
|
|
1656
2235
|
const clientIP = req.headers["x-real-ip"] || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "";
|
|
@@ -1668,7 +2247,8 @@ var MoltsPayServer = class {
|
|
|
1668
2247
|
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
1669
2248
|
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1670
2249
|
const x402Header = req.headers[PAYMENT_HEADER];
|
|
1671
|
-
|
|
2250
|
+
const proofHeader = req.headers[ALIPAY_PAYMENT_PROOF_HEADER];
|
|
2251
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader);
|
|
1672
2252
|
}
|
|
1673
2253
|
this.sendJson(res, 404, { error: "Not found" });
|
|
1674
2254
|
} catch (err) {
|
|
@@ -1765,7 +2345,7 @@ var MoltsPayServer = class {
|
|
|
1765
2345
|
/**
|
|
1766
2346
|
* POST /execute - Execute service with x402 payment
|
|
1767
2347
|
*/
|
|
1768
|
-
async handleExecute(body, paymentHeader, res) {
|
|
2348
|
+
async handleExecute(body, paymentHeader, res, proofHeader) {
|
|
1769
2349
|
const { service, params } = body;
|
|
1770
2350
|
if (!service) {
|
|
1771
2351
|
return this.sendJson(res, 400, { error: "Missing service" });
|
|
@@ -1779,6 +2359,15 @@ var MoltsPayServer = class {
|
|
|
1779
2359
|
return this.sendJson(res, 400, { error: `Missing required param: ${key}` });
|
|
1780
2360
|
}
|
|
1781
2361
|
}
|
|
2362
|
+
if (proofHeader) {
|
|
2363
|
+
const alipayPayment = {
|
|
2364
|
+
x402Version: X402_VERSION2,
|
|
2365
|
+
scheme: ALIPAY_SCHEME,
|
|
2366
|
+
network: ALIPAY_NETWORK,
|
|
2367
|
+
payload: proofHeader
|
|
2368
|
+
};
|
|
2369
|
+
return this.handleAlipayExecute(skill, params || {}, alipayPayment, res);
|
|
2370
|
+
}
|
|
1782
2371
|
if (!paymentHeader) {
|
|
1783
2372
|
return this.sendPaymentRequired(skill.config, res);
|
|
1784
2373
|
}
|
|
@@ -1789,6 +2378,11 @@ var MoltsPayServer = class {
|
|
|
1789
2378
|
} catch {
|
|
1790
2379
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
1791
2380
|
}
|
|
2381
|
+
const payScheme = payment.accepted?.scheme || payment.scheme;
|
|
2382
|
+
const payNetwork = payment.accepted?.network || payment.network;
|
|
2383
|
+
if (payScheme === ALIPAY_SCHEME || (payNetwork ? isAlipayChainId(payNetwork) : false)) {
|
|
2384
|
+
return this.handleAlipayExecute(skill, params || {}, payment, res);
|
|
2385
|
+
}
|
|
1792
2386
|
const validation = this.validatePayment(payment, skill.config);
|
|
1793
2387
|
if (!validation.valid) {
|
|
1794
2388
|
return this.sendJson(res, 402, { error: validation.error });
|
|
@@ -1853,6 +2447,14 @@ var MoltsPayServer = class {
|
|
|
1853
2447
|
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1854
2448
|
} catch (err) {
|
|
1855
2449
|
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
2450
|
+
settlement = { success: false, error: err.message, facilitator: "none" };
|
|
2451
|
+
}
|
|
2452
|
+
if (!settlement?.success) {
|
|
2453
|
+
return this.sendJson(res, 402, {
|
|
2454
|
+
error: "Payment settlement failed",
|
|
2455
|
+
message: settlement?.error || "Settlement returned no success state",
|
|
2456
|
+
facilitator: settlement?.facilitator
|
|
2457
|
+
});
|
|
1856
2458
|
}
|
|
1857
2459
|
}
|
|
1858
2460
|
const responseHeaders = {};
|
|
@@ -1873,13 +2475,111 @@ var MoltsPayServer = class {
|
|
|
1873
2475
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
1874
2476
|
}, responseHeaders);
|
|
1875
2477
|
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Execute a service paid via the Alipay AI 收 fiat rail (2.0.0).
|
|
2480
|
+
*
|
|
2481
|
+
* Differs from the EVM/SVM path: no token detection, no EIP-3009/permit
|
|
2482
|
+
* validation. Verify hits the Alipay Open API (`payment.verify`). Settlement
|
|
2483
|
+
* (`fulfillment.confirm`) is FIRE-AND-FORGET per ALIPAY-INTEGRATION-DESIGN
|
|
2484
|
+
* §5.1: a confirm failure is logged but does NOT fail the already-delivered
|
|
2485
|
+
* response (the buyer's payment proof was already verified).
|
|
2486
|
+
*/
|
|
2487
|
+
async handleAlipayExecute(skill, params, payment, res) {
|
|
2488
|
+
if (!this.alipayFacilitator) {
|
|
2489
|
+
return this.sendJson(res, 402, { error: "Alipay rail not configured on this server" });
|
|
2490
|
+
}
|
|
2491
|
+
const requirements = {
|
|
2492
|
+
scheme: ALIPAY_SCHEME,
|
|
2493
|
+
network: ALIPAY_NETWORK,
|
|
2494
|
+
asset: "CNY",
|
|
2495
|
+
amount: skill.config.alipay?.price_cny || "0",
|
|
2496
|
+
payTo: this.manifest.provider.alipay?.seller_id || "",
|
|
2497
|
+
maxTimeoutSeconds: 1800
|
|
2498
|
+
};
|
|
2499
|
+
console.log(`[MoltsPay] Verifying Alipay payment...`);
|
|
2500
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
2501
|
+
if (!verifyResult.valid) {
|
|
2502
|
+
return this.sendJson(res, 402, {
|
|
2503
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2504
|
+
facilitator: verifyResult.facilitator
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
console.log(`[MoltsPay] Alipay payment verified by ${verifyResult.facilitator}`);
|
|
2508
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2509
|
+
console.log(`[MoltsPay] Executing skill: ${skill.id} (timeout: ${timeoutSeconds}s)`);
|
|
2510
|
+
let result;
|
|
2511
|
+
try {
|
|
2512
|
+
result = await Promise.race([
|
|
2513
|
+
skill.handler(params),
|
|
2514
|
+
new Promise(
|
|
2515
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2516
|
+
)
|
|
2517
|
+
]);
|
|
2518
|
+
} catch (err) {
|
|
2519
|
+
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
2520
|
+
return this.sendJson(res, 500, {
|
|
2521
|
+
error: "Service execution failed",
|
|
2522
|
+
message: err.message
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
let settlement;
|
|
2526
|
+
try {
|
|
2527
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2528
|
+
if (settlement.success) {
|
|
2529
|
+
console.log(`[MoltsPay] Alipay fulfillment confirmed: ${settlement.transaction}`);
|
|
2530
|
+
} else {
|
|
2531
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm failed (non-fatal): ${settlement.error}`);
|
|
2532
|
+
}
|
|
2533
|
+
} catch (err) {
|
|
2534
|
+
console.error(`[MoltsPay] Alipay fulfillment confirm threw (non-fatal): ${err.message}`);
|
|
2535
|
+
settlement = { success: false, error: err.message, facilitator: "alipay" };
|
|
2536
|
+
}
|
|
2537
|
+
const responseHeaders = {};
|
|
2538
|
+
if (settlement.success) {
|
|
2539
|
+
responseHeaders[PAYMENT_RESPONSE_HEADER] = Buffer.from(JSON.stringify({
|
|
2540
|
+
success: true,
|
|
2541
|
+
transaction: settlement.transaction,
|
|
2542
|
+
network: ALIPAY_NETWORK,
|
|
2543
|
+
facilitator: settlement.facilitator
|
|
2544
|
+
})).toString("base64");
|
|
2545
|
+
}
|
|
2546
|
+
this.sendJson(res, 200, {
|
|
2547
|
+
success: true,
|
|
2548
|
+
result,
|
|
2549
|
+
payment: settlement.success ? { transaction: settlement.transaction, status: "fulfilled", facilitator: settlement.facilitator } : { status: "delivered_unconfirmed", error: settlement.error }
|
|
2550
|
+
}, responseHeaders);
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Build the Alipay 402 challenge for a service, or null when the alipay rail
|
|
2554
|
+
* isn't configured for this server or this service. Returns the x402
|
|
2555
|
+
* `accepts[]` entry plus the Base64URL `Payment-Needed` header value so the
|
|
2556
|
+
* 402 responders can dual-emit both the x402 and legacy alipay-bot formats.
|
|
2557
|
+
*/
|
|
2558
|
+
async buildAlipayChallenge(config) {
|
|
2559
|
+
if (!this.alipayFacilitator || !config.alipay) return null;
|
|
2560
|
+
try {
|
|
2561
|
+
const req = await this.alipayFacilitator.createPaymentRequirements({
|
|
2562
|
+
serviceId: config.alipay.service_id || this.manifest.provider.alipay.service_id_default,
|
|
2563
|
+
priceCny: config.alipay.price_cny,
|
|
2564
|
+
goodsName: config.alipay.goods_name,
|
|
2565
|
+
resourceId: `/execute?service=${config.id}`
|
|
2566
|
+
});
|
|
2567
|
+
return { accepts: req.x402Accepts, paymentNeededHeader: req.paymentNeededHeader };
|
|
2568
|
+
} catch (err) {
|
|
2569
|
+
console.error(`[MoltsPay] Alipay challenge build failed for ${config.id}: ${err.message}`);
|
|
2570
|
+
return null;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
1876
2573
|
/**
|
|
1877
2574
|
* Handle MPP (Machine Payments Protocol) request
|
|
1878
2575
|
* Supports both x402 and MPP protocols on service endpoints
|
|
1879
2576
|
*/
|
|
1880
|
-
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
2577
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res, proofHeader) {
|
|
1881
2578
|
const config = skill.config;
|
|
1882
2579
|
const params = body || {};
|
|
2580
|
+
if (proofHeader) {
|
|
2581
|
+
return await this.handleExecute({ service: config.id, params }, void 0, res, proofHeader);
|
|
2582
|
+
}
|
|
1883
2583
|
if (x402Header) {
|
|
1884
2584
|
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
1885
2585
|
}
|
|
@@ -1985,7 +2685,7 @@ var MoltsPayServer = class {
|
|
|
1985
2685
|
/**
|
|
1986
2686
|
* Return 402 with both x402 and MPP payment requirements
|
|
1987
2687
|
*/
|
|
1988
|
-
sendMPPPaymentRequired(config, res) {
|
|
2688
|
+
async sendMPPPaymentRequired(config, res) {
|
|
1989
2689
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
1990
2690
|
const providerChains = this.getProviderChains();
|
|
1991
2691
|
const accepts = [];
|
|
@@ -1996,6 +2696,10 @@ var MoltsPayServer = class {
|
|
|
1996
2696
|
}
|
|
1997
2697
|
}
|
|
1998
2698
|
}
|
|
2699
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
2700
|
+
if (alipayChallenge) {
|
|
2701
|
+
accepts.push(alipayChallenge.accepts);
|
|
2702
|
+
}
|
|
1999
2703
|
const x402PaymentRequired = {
|
|
2000
2704
|
x402Version: X402_VERSION2,
|
|
2001
2705
|
accepts,
|
|
@@ -2023,7 +2727,7 @@ var MoltsPayServer = class {
|
|
|
2023
2727
|
};
|
|
2024
2728
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2025
2729
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2026
|
-
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2730
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${headerSafe(this.manifest.provider.name)}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
2027
2731
|
}
|
|
2028
2732
|
const headers = {
|
|
2029
2733
|
"Content-Type": "application/problem+json",
|
|
@@ -2032,6 +2736,9 @@ var MoltsPayServer = class {
|
|
|
2032
2736
|
if (mppWwwAuth) {
|
|
2033
2737
|
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
2034
2738
|
}
|
|
2739
|
+
if (alipayChallenge) {
|
|
2740
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
2741
|
+
}
|
|
2035
2742
|
res.writeHead(402, headers);
|
|
2036
2743
|
res.end(JSON.stringify({
|
|
2037
2744
|
type: "https://paymentauth.org/problems/payment-required",
|
|
@@ -2058,7 +2765,7 @@ var MoltsPayServer = class {
|
|
|
2058
2765
|
* Return 402 with x402 payment requirements (v2 format)
|
|
2059
2766
|
* Includes requirements for all chains and all accepted currencies
|
|
2060
2767
|
*/
|
|
2061
|
-
sendPaymentRequired(config, res) {
|
|
2768
|
+
async sendPaymentRequired(config, res) {
|
|
2062
2769
|
const acceptedTokens = getAcceptedCurrencies(config);
|
|
2063
2770
|
const providerChains = this.getProviderChains();
|
|
2064
2771
|
const accepts = [];
|
|
@@ -2069,6 +2776,10 @@ var MoltsPayServer = class {
|
|
|
2069
2776
|
}
|
|
2070
2777
|
}
|
|
2071
2778
|
}
|
|
2779
|
+
const alipayChallenge = await this.buildAlipayChallenge(config);
|
|
2780
|
+
if (alipayChallenge) {
|
|
2781
|
+
accepts.push(alipayChallenge.accepts);
|
|
2782
|
+
}
|
|
2072
2783
|
const acceptedChains = providerChains.map((c) => {
|
|
2073
2784
|
if (c.network === "eip155:8453") return "base";
|
|
2074
2785
|
if (c.network === "eip155:137") return "polygon";
|
|
@@ -2086,10 +2797,14 @@ var MoltsPayServer = class {
|
|
|
2086
2797
|
}
|
|
2087
2798
|
};
|
|
2088
2799
|
const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
2089
|
-
|
|
2800
|
+
const headers = {
|
|
2090
2801
|
"Content-Type": "application/json",
|
|
2091
2802
|
[PAYMENT_REQUIRED_HEADER]: encoded
|
|
2092
|
-
}
|
|
2803
|
+
};
|
|
2804
|
+
if (alipayChallenge) {
|
|
2805
|
+
headers[ALIPAY_PAYMENT_NEEDED_HEADER] = alipayChallenge.paymentNeededHeader;
|
|
2806
|
+
}
|
|
2807
|
+
res.writeHead(402, headers);
|
|
2093
2808
|
res.end(JSON.stringify({
|
|
2094
2809
|
error: "Payment required",
|
|
2095
2810
|
message: `Service requires $${config.price} ${config.currency}`,
|
|
@@ -2107,7 +2822,7 @@ var MoltsPayServer = class {
|
|
|
2107
2822
|
}
|
|
2108
2823
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
2109
2824
|
const network = payment.accepted?.network || payment.network || this.networkId;
|
|
2110
|
-
if (scheme !== "exact") {
|
|
2825
|
+
if (scheme !== "exact" && scheme !== "permit") {
|
|
2111
2826
|
return { valid: false, error: `Unsupported scheme: ${scheme}` };
|
|
2112
2827
|
}
|
|
2113
2828
|
if (!this.isNetworkAccepted(network)) {
|
|
@@ -2129,8 +2844,10 @@ var MoltsPayServer = class {
|
|
|
2129
2844
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
2130
2845
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2131
2846
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
2847
|
+
const isTempo = selectedNetwork === "eip155:42431";
|
|
2848
|
+
const scheme = isTempo ? "permit" : "exact";
|
|
2132
2849
|
const requirements = {
|
|
2133
|
-
scheme
|
|
2850
|
+
scheme,
|
|
2134
2851
|
network: selectedNetwork,
|
|
2135
2852
|
asset: tokenAddress,
|
|
2136
2853
|
amount: amountInUnits,
|
|
@@ -2158,6 +2875,16 @@ var MoltsPayServer = class {
|
|
|
2158
2875
|
};
|
|
2159
2876
|
}
|
|
2160
2877
|
}
|
|
2878
|
+
if (isTempo) {
|
|
2879
|
+
const tempoFacilitator = this.registry.get("tempo");
|
|
2880
|
+
const tempoSpender = tempoFacilitator?.getSpenderAddress?.();
|
|
2881
|
+
if (tempoSpender) {
|
|
2882
|
+
requirements.extra = {
|
|
2883
|
+
...requirements.extra || {},
|
|
2884
|
+
tempoSpender
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2161
2888
|
return requirements;
|
|
2162
2889
|
}
|
|
2163
2890
|
/**
|
|
@@ -2184,12 +2911,12 @@ var MoltsPayServer = class {
|
|
|
2184
2911
|
return accepted.includes(token);
|
|
2185
2912
|
}
|
|
2186
2913
|
async readBody(req) {
|
|
2187
|
-
return new Promise((
|
|
2914
|
+
return new Promise((resolve2, reject) => {
|
|
2188
2915
|
let body = "";
|
|
2189
2916
|
req.on("data", (chunk) => body += chunk);
|
|
2190
2917
|
req.on("end", () => {
|
|
2191
2918
|
try {
|
|
2192
|
-
|
|
2919
|
+
resolve2(body ? JSON.parse(body) : {});
|
|
2193
2920
|
} catch {
|
|
2194
2921
|
reject(new Error("Invalid JSON"));
|
|
2195
2922
|
}
|
|
@@ -2292,7 +3019,7 @@ var MoltsPayServer = class {
|
|
|
2292
3019
|
}
|
|
2293
3020
|
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
2294
3021
|
const network = payment.accepted?.network || payment.network;
|
|
2295
|
-
if (scheme !== "exact") {
|
|
3022
|
+
if (scheme !== "exact" && scheme !== "permit") {
|
|
2296
3023
|
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
2297
3024
|
}
|
|
2298
3025
|
const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
|
|
@@ -2447,7 +3174,7 @@ var MoltsPayServer = class {
|
|
|
2447
3174
|
};
|
|
2448
3175
|
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2449
3176
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2450
|
-
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
3177
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${headerSafe(config.name)}", expires="${expiresAt}"`;
|
|
2451
3178
|
res.writeHead(402, {
|
|
2452
3179
|
"Content-Type": "application/problem+json",
|
|
2453
3180
|
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
@@ -2612,11 +3339,11 @@ var MoltsPayServer = class {
|
|
|
2612
3339
|
}
|
|
2613
3340
|
};
|
|
2614
3341
|
|
|
2615
|
-
// src/client/index.ts
|
|
3342
|
+
// src/client/node/index.ts
|
|
2616
3343
|
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
|
|
2617
|
-
import { homedir as
|
|
2618
|
-
import { join as
|
|
2619
|
-
import { Wallet, ethers } from "ethers";
|
|
3344
|
+
import { homedir as homedir3 } from "os";
|
|
3345
|
+
import { join as join5 } from "path";
|
|
3346
|
+
import { Wallet as Wallet2, ethers as ethers3 } from "ethers";
|
|
2620
3347
|
|
|
2621
3348
|
// src/wallet/solana.ts
|
|
2622
3349
|
import { Keypair as Keypair3, PublicKey as PublicKey3, LAMPORTS_PER_SOL } from "@solana/web3.js";
|
|
@@ -2645,11 +3372,924 @@ function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
|
2645
3372
|
}
|
|
2646
3373
|
}
|
|
2647
3374
|
|
|
2648
|
-
// src/client/index.ts
|
|
3375
|
+
// src/client/node/index.ts
|
|
2649
3376
|
import { PublicKey as PublicKey4 } from "@solana/web3.js";
|
|
3377
|
+
|
|
3378
|
+
// src/client/core/types.ts
|
|
2650
3379
|
var X402_VERSION3 = 2;
|
|
2651
3380
|
var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
|
|
2652
3381
|
var PAYMENT_HEADER2 = "x-payment";
|
|
3382
|
+
|
|
3383
|
+
// src/client/core/chain-map.ts
|
|
3384
|
+
var NETWORK_TO_CHAIN = {
|
|
3385
|
+
"eip155:8453": "base",
|
|
3386
|
+
"eip155:137": "polygon",
|
|
3387
|
+
"eip155:84532": "base_sepolia",
|
|
3388
|
+
"eip155:42431": "tempo_moderato",
|
|
3389
|
+
"eip155:56": "bnb",
|
|
3390
|
+
"eip155:97": "bnb_testnet",
|
|
3391
|
+
"solana:mainnet": "solana",
|
|
3392
|
+
"solana:devnet": "solana_devnet"
|
|
3393
|
+
};
|
|
3394
|
+
var CHAIN_TO_NETWORK2 = Object.fromEntries(
|
|
3395
|
+
Object.entries(NETWORK_TO_CHAIN).map(([network, chain]) => [chain, network])
|
|
3396
|
+
);
|
|
3397
|
+
function networkToChainName(network) {
|
|
3398
|
+
return NETWORK_TO_CHAIN[network] ?? null;
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// src/client/core/base64.ts
|
|
3402
|
+
var BufferCtor = globalThis.Buffer;
|
|
3403
|
+
|
|
3404
|
+
// src/client/core/errors.ts
|
|
3405
|
+
var MoltsPayError = class extends Error {
|
|
3406
|
+
code;
|
|
3407
|
+
constructor(code, message) {
|
|
3408
|
+
super(message);
|
|
3409
|
+
this.name = "MoltsPayError";
|
|
3410
|
+
this.code = code;
|
|
3411
|
+
}
|
|
3412
|
+
};
|
|
3413
|
+
|
|
3414
|
+
// src/client/core/eip3009.ts
|
|
3415
|
+
var EIP3009_TYPES = {
|
|
3416
|
+
TransferWithAuthorization: [
|
|
3417
|
+
{ name: "from", type: "address" },
|
|
3418
|
+
{ name: "to", type: "address" },
|
|
3419
|
+
{ name: "value", type: "uint256" },
|
|
3420
|
+
{ name: "validAfter", type: "uint256" },
|
|
3421
|
+
{ name: "validBefore", type: "uint256" },
|
|
3422
|
+
{ name: "nonce", type: "bytes32" }
|
|
3423
|
+
]
|
|
3424
|
+
};
|
|
3425
|
+
function buildEIP3009TypedData(args) {
|
|
3426
|
+
const validAfter = args.validAfter ?? "0";
|
|
3427
|
+
const validBefore = args.validBefore ?? (Math.floor(Date.now() / 1e3) + 3600).toString();
|
|
3428
|
+
const authorization = {
|
|
3429
|
+
from: args.from,
|
|
3430
|
+
to: args.to,
|
|
3431
|
+
value: args.value,
|
|
3432
|
+
validAfter,
|
|
3433
|
+
validBefore,
|
|
3434
|
+
nonce: args.nonce
|
|
3435
|
+
};
|
|
3436
|
+
return {
|
|
3437
|
+
domain: {
|
|
3438
|
+
name: args.tokenName,
|
|
3439
|
+
version: args.tokenVersion,
|
|
3440
|
+
chainId: args.chainId,
|
|
3441
|
+
verifyingContract: args.tokenAddress
|
|
3442
|
+
},
|
|
3443
|
+
types: EIP3009_TYPES,
|
|
3444
|
+
primaryType: "TransferWithAuthorization",
|
|
3445
|
+
message: authorization
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
// src/client/core/bnb-intent.ts
|
|
3450
|
+
var BNB_INTENT_TYPES = {
|
|
3451
|
+
PaymentIntent: [
|
|
3452
|
+
{ name: "from", type: "address" },
|
|
3453
|
+
{ name: "to", type: "address" },
|
|
3454
|
+
{ name: "amount", type: "uint256" },
|
|
3455
|
+
{ name: "token", type: "address" },
|
|
3456
|
+
{ name: "service", type: "string" },
|
|
3457
|
+
{ name: "nonce", type: "uint256" },
|
|
3458
|
+
{ name: "deadline", type: "uint256" }
|
|
3459
|
+
]
|
|
3460
|
+
};
|
|
3461
|
+
var BNB_DOMAIN_NAME = "MoltsPay";
|
|
3462
|
+
var BNB_DOMAIN_VERSION = "1";
|
|
3463
|
+
function buildBnbIntentTypedData(args) {
|
|
3464
|
+
const intent = {
|
|
3465
|
+
from: args.from,
|
|
3466
|
+
to: args.to,
|
|
3467
|
+
amount: args.amount,
|
|
3468
|
+
token: args.tokenAddress,
|
|
3469
|
+
service: args.service,
|
|
3470
|
+
nonce: args.nonce,
|
|
3471
|
+
deadline: args.deadline
|
|
3472
|
+
};
|
|
3473
|
+
return {
|
|
3474
|
+
domain: {
|
|
3475
|
+
name: BNB_DOMAIN_NAME,
|
|
3476
|
+
version: BNB_DOMAIN_VERSION,
|
|
3477
|
+
chainId: args.chainId
|
|
3478
|
+
},
|
|
3479
|
+
types: BNB_INTENT_TYPES,
|
|
3480
|
+
primaryType: "PaymentIntent",
|
|
3481
|
+
message: intent
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// src/client/node/signer.ts
|
|
3486
|
+
import { ethers as ethers2 } from "ethers";
|
|
3487
|
+
import { Transaction as Transaction2 } from "@solana/web3.js";
|
|
3488
|
+
var NodeSigner = class {
|
|
3489
|
+
evmWallet;
|
|
3490
|
+
getSolanaKeypair;
|
|
3491
|
+
constructor(evmWallet, options = {}) {
|
|
3492
|
+
this.evmWallet = evmWallet;
|
|
3493
|
+
this.getSolanaKeypair = options.getSolanaKeypair ?? (() => null);
|
|
3494
|
+
}
|
|
3495
|
+
async getEvmAddress() {
|
|
3496
|
+
return this.evmWallet.address;
|
|
3497
|
+
}
|
|
3498
|
+
async getSolanaAddress() {
|
|
3499
|
+
const kp = this.getSolanaKeypair();
|
|
3500
|
+
return kp ? kp.publicKey.toBase58() : null;
|
|
3501
|
+
}
|
|
3502
|
+
async signTypedData(envelope) {
|
|
3503
|
+
const mutableTypes = {};
|
|
3504
|
+
for (const [key, fields] of Object.entries(envelope.types)) {
|
|
3505
|
+
mutableTypes[key] = [...fields];
|
|
3506
|
+
}
|
|
3507
|
+
return this.evmWallet.signTypedData(
|
|
3508
|
+
envelope.domain,
|
|
3509
|
+
mutableTypes,
|
|
3510
|
+
envelope.message
|
|
3511
|
+
);
|
|
3512
|
+
}
|
|
3513
|
+
async sendEvmTransaction(args) {
|
|
3514
|
+
const chain = findChainByChainId(args.chainId);
|
|
3515
|
+
if (!chain) {
|
|
3516
|
+
throw new Error(`sendEvmTransaction: unknown chainId ${args.chainId}`);
|
|
3517
|
+
}
|
|
3518
|
+
const provider = new ethers2.JsonRpcProvider(chain.rpc);
|
|
3519
|
+
const connected = this.evmWallet.connect(provider);
|
|
3520
|
+
const tx = await connected.sendTransaction({
|
|
3521
|
+
to: args.to,
|
|
3522
|
+
data: args.data,
|
|
3523
|
+
value: args.value ? BigInt(args.value) : 0n
|
|
3524
|
+
});
|
|
3525
|
+
return tx.hash;
|
|
3526
|
+
}
|
|
3527
|
+
async signSolanaTransaction(args) {
|
|
3528
|
+
const kp = this.getSolanaKeypair();
|
|
3529
|
+
if (!kp) {
|
|
3530
|
+
throw new Error("signSolanaTransaction: no Solana wallet configured");
|
|
3531
|
+
}
|
|
3532
|
+
const tx = Transaction2.from(Buffer.from(args.transactionBase64, "base64"));
|
|
3533
|
+
if (args.partialSign) {
|
|
3534
|
+
tx.partialSign(kp);
|
|
3535
|
+
} else {
|
|
3536
|
+
tx.sign(kp);
|
|
3537
|
+
}
|
|
3538
|
+
return tx.serialize({ requireAllSignatures: false }).toString("base64");
|
|
3539
|
+
}
|
|
3540
|
+
};
|
|
3541
|
+
function findChainByChainId(chainId) {
|
|
3542
|
+
for (const cfg of Object.values(CHAINS)) {
|
|
3543
|
+
if (cfg.chainId === chainId) {
|
|
3544
|
+
return cfg;
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
return void 0;
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// src/client/alipay/index.ts
|
|
3551
|
+
import { randomUUID } from "crypto";
|
|
3552
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
3553
|
+
import { homedir as homedir2 } from "os";
|
|
3554
|
+
import { join as join4 } from "path";
|
|
3555
|
+
|
|
3556
|
+
// src/client/alipay/cli.ts
|
|
3557
|
+
import { spawn } from "child_process";
|
|
3558
|
+
|
|
3559
|
+
// src/client/alipay/log.ts
|
|
3560
|
+
var RANK = { off: 0, info: 1, debug: 2 };
|
|
3561
|
+
function normalizeLevel(v) {
|
|
3562
|
+
const s = (v ?? "").toLowerCase();
|
|
3563
|
+
return s === "info" || s === "debug" ? s : "off";
|
|
3564
|
+
}
|
|
3565
|
+
var level = normalizeLevel(process.env.MOLTSPAY_ALIPAY_LOG);
|
|
3566
|
+
var sink = (line) => {
|
|
3567
|
+
process.stderr.write(line + "\n");
|
|
3568
|
+
};
|
|
3569
|
+
function setAlipayLogLevel(next) {
|
|
3570
|
+
level = next;
|
|
3571
|
+
}
|
|
3572
|
+
function getAlipayLogLevel() {
|
|
3573
|
+
return level;
|
|
3574
|
+
}
|
|
3575
|
+
function setAlipayLogSink(fn) {
|
|
3576
|
+
sink = fn;
|
|
3577
|
+
}
|
|
3578
|
+
function enabledFor(want) {
|
|
3579
|
+
return RANK[level] >= RANK[want];
|
|
3580
|
+
}
|
|
3581
|
+
function render(event, fields) {
|
|
3582
|
+
const parts = ["[moltspay:alipay]", String(fields.ts), event];
|
|
3583
|
+
if (fields.flow) parts.push(`flow=${fields.flow}`);
|
|
3584
|
+
if (fields.step) parts.push(`step=${fields.step}`);
|
|
3585
|
+
if (typeof fields.ms === "number") parts.push(`${fields.ms}ms`);
|
|
3586
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
3587
|
+
if (["ts", "level", "event", "flow", "step", "ms"].includes(k)) continue;
|
|
3588
|
+
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
3589
|
+
}
|
|
3590
|
+
return parts.join(" ");
|
|
3591
|
+
}
|
|
3592
|
+
function emit(lvl, event, fields) {
|
|
3593
|
+
if (!enabledFor(lvl)) return;
|
|
3594
|
+
const full = { ts: (/* @__PURE__ */ new Date()).toISOString(), level: lvl, event, ...fields };
|
|
3595
|
+
try {
|
|
3596
|
+
sink(render(event, full), full);
|
|
3597
|
+
} catch {
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
var alipayLog = {
|
|
3601
|
+
info: (event, fields = {}) => emit("info", event, fields),
|
|
3602
|
+
debug: (event, fields = {}) => emit("debug", event, fields),
|
|
3603
|
+
/** True if anything at all would be logged (cheap guard for hot paths). */
|
|
3604
|
+
enabled: () => level !== "off"
|
|
3605
|
+
};
|
|
3606
|
+
async function timeStep(step, flow, fn) {
|
|
3607
|
+
if (!alipayLog.enabled()) return fn();
|
|
3608
|
+
const t0 = Date.now();
|
|
3609
|
+
alipayLog.debug("step.start", { flow, step });
|
|
3610
|
+
try {
|
|
3611
|
+
const out = await fn();
|
|
3612
|
+
alipayLog.info("step.end", { flow, step, ms: Date.now() - t0, ok: true });
|
|
3613
|
+
return out;
|
|
3614
|
+
} catch (err) {
|
|
3615
|
+
alipayLog.info("step.end", {
|
|
3616
|
+
flow,
|
|
3617
|
+
step,
|
|
3618
|
+
ms: Date.now() - t0,
|
|
3619
|
+
ok: false,
|
|
3620
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3621
|
+
});
|
|
3622
|
+
throw err;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// src/client/alipay/cli.ts
|
|
3627
|
+
var ALLOWED_ENV = /* @__PURE__ */ new Set([
|
|
3628
|
+
"AIPAY_OUTPUT_CHANNEL",
|
|
3629
|
+
"AIPAY_SESSION_ID",
|
|
3630
|
+
"AIPAY_FRAMEWORK",
|
|
3631
|
+
"AIPAY_MODEL",
|
|
3632
|
+
"AIPAY_OS",
|
|
3633
|
+
// Minimal survival set for spawn to find the binary and a home dir.
|
|
3634
|
+
"PATH",
|
|
3635
|
+
"HOME"
|
|
3636
|
+
]);
|
|
3637
|
+
function filterEnv(env) {
|
|
3638
|
+
return Object.fromEntries(
|
|
3639
|
+
Object.entries(env).filter(([k]) => ALLOWED_ENV.has(k))
|
|
3640
|
+
);
|
|
3641
|
+
}
|
|
3642
|
+
function makeLineSplitter(onLine) {
|
|
3643
|
+
let buf = "";
|
|
3644
|
+
return (chunk) => {
|
|
3645
|
+
buf += chunk.toString("utf-8");
|
|
3646
|
+
let nl;
|
|
3647
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
3648
|
+
onLine(buf.slice(0, nl));
|
|
3649
|
+
buf = buf.slice(nl + 1);
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
function profileEnv(step, flow) {
|
|
3654
|
+
const want = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE;
|
|
3655
|
+
const hook = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_HOOK;
|
|
3656
|
+
if (!want || !hook) return {};
|
|
3657
|
+
const steps = want.split(",").map((s) => s.trim());
|
|
3658
|
+
if (!steps.includes("all") && !steps.includes(step)) return {};
|
|
3659
|
+
const dir = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_DIR || "/tmp";
|
|
3660
|
+
const safe = `${flow ?? "noflow"}-${step}-${Date.now()}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3661
|
+
const out = `${dir}/cli-profile-${safe}.json`;
|
|
3662
|
+
alipayLog.info("cli.profile", { flow, step, out });
|
|
3663
|
+
return {
|
|
3664
|
+
NODE_OPTIONS: `--require=${hook}`,
|
|
3665
|
+
MOLTSPAY_CLI_PROFILE_OUT: out
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
var runCli = (args, opts = {}) => {
|
|
3669
|
+
const bin = opts.bin ?? "alipay-bot";
|
|
3670
|
+
const step = opts.step ?? args[0] ?? "(no-arg)";
|
|
3671
|
+
const startedAt = Date.now();
|
|
3672
|
+
const lines = [];
|
|
3673
|
+
const collect = (line) => {
|
|
3674
|
+
lines.push(line);
|
|
3675
|
+
alipayLog.debug("cli.line", {
|
|
3676
|
+
flow: opts.flow,
|
|
3677
|
+
step,
|
|
3678
|
+
ms: Date.now() - startedAt,
|
|
3679
|
+
preview: line.slice(0, 120)
|
|
3680
|
+
});
|
|
3681
|
+
opts.onLine?.(line);
|
|
3682
|
+
};
|
|
3683
|
+
alipayLog.debug("step.start", { flow: opts.flow, step });
|
|
3684
|
+
return new Promise((resolve2, reject) => {
|
|
3685
|
+
const child = spawn(bin, args, {
|
|
3686
|
+
env: { ...filterEnv(process.env), ...profileEnv(step, opts.flow), ...opts.env ?? {} }
|
|
3687
|
+
});
|
|
3688
|
+
if (opts.signal) {
|
|
3689
|
+
if (opts.signal.aborted) child.kill("SIGTERM");
|
|
3690
|
+
opts.signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
|
|
3691
|
+
}
|
|
3692
|
+
let sawByte = false;
|
|
3693
|
+
const onChunk = (stream) => (chunk) => {
|
|
3694
|
+
const ms = Date.now() - startedAt;
|
|
3695
|
+
if (!sawByte) {
|
|
3696
|
+
sawByte = true;
|
|
3697
|
+
alipayLog.debug("cli.firstbyte", { flow: opts.flow, step, ms });
|
|
3698
|
+
}
|
|
3699
|
+
alipayLog.debug("cli.chunk", { flow: opts.flow, step, ms, stream, bytes: chunk.length });
|
|
3700
|
+
};
|
|
3701
|
+
const onStdout = makeLineSplitter(collect);
|
|
3702
|
+
const onStderr = makeLineSplitter(collect);
|
|
3703
|
+
child.stdout?.on("data", onChunk("out"));
|
|
3704
|
+
child.stderr?.on("data", onChunk("err"));
|
|
3705
|
+
child.stdout?.on("data", onStdout);
|
|
3706
|
+
child.stderr?.on("data", onStderr);
|
|
3707
|
+
child.on("error", (err) => {
|
|
3708
|
+
alipayLog.info("cli.exit", {
|
|
3709
|
+
flow: opts.flow,
|
|
3710
|
+
step,
|
|
3711
|
+
ms: Date.now() - startedAt,
|
|
3712
|
+
ok: false,
|
|
3713
|
+
err: err.message
|
|
3714
|
+
});
|
|
3715
|
+
reject(err);
|
|
3716
|
+
});
|
|
3717
|
+
child.on("close", (code) => {
|
|
3718
|
+
alipayLog.info("cli.exit", {
|
|
3719
|
+
flow: opts.flow,
|
|
3720
|
+
step,
|
|
3721
|
+
ms: Date.now() - startedAt,
|
|
3722
|
+
ok: (code ?? 1) === 0,
|
|
3723
|
+
code: code ?? 1
|
|
3724
|
+
});
|
|
3725
|
+
resolve2({ exitCode: code ?? 1, lines });
|
|
3726
|
+
});
|
|
3727
|
+
});
|
|
3728
|
+
};
|
|
3729
|
+
|
|
3730
|
+
// src/client/alipay/install.ts
|
|
3731
|
+
import { execFile } from "child_process";
|
|
3732
|
+
import { promisify } from "util";
|
|
3733
|
+
|
|
3734
|
+
// src/client/alipay/errors.ts
|
|
3735
|
+
var AlipayCliNotFoundError = class extends MoltsPayError {
|
|
3736
|
+
constructor(message) {
|
|
3737
|
+
super("ALIPAY_CLI_NOT_FOUND", message);
|
|
3738
|
+
this.name = "AlipayCliNotFoundError";
|
|
3739
|
+
}
|
|
3740
|
+
};
|
|
3741
|
+
var AlipayCliVersionError = class extends MoltsPayError {
|
|
3742
|
+
constructor(message) {
|
|
3743
|
+
super("ALIPAY_CLI_VERSION", message);
|
|
3744
|
+
this.name = "AlipayCliVersionError";
|
|
3745
|
+
}
|
|
3746
|
+
};
|
|
3747
|
+
var NeedsWalletSetupError = class extends MoltsPayError {
|
|
3748
|
+
constructor(message = "Alipay wallet not set up. Run: moltspay alipay apply") {
|
|
3749
|
+
super("ALIPAY_NEEDS_WALLET_SETUP", message);
|
|
3750
|
+
this.name = "NeedsWalletSetupError";
|
|
3751
|
+
}
|
|
3752
|
+
};
|
|
3753
|
+
var AlipayPaymentRejectedError = class extends MoltsPayError {
|
|
3754
|
+
constructor(message) {
|
|
3755
|
+
super("ALIPAY_PAYMENT_REJECTED", message);
|
|
3756
|
+
this.name = "AlipayPaymentRejectedError";
|
|
3757
|
+
}
|
|
3758
|
+
};
|
|
3759
|
+
var AlipayPaymentTimeoutError = class extends MoltsPayError {
|
|
3760
|
+
constructor(message) {
|
|
3761
|
+
super("ALIPAY_PAYMENT_TIMEOUT", message);
|
|
3762
|
+
this.name = "AlipayPaymentTimeoutError";
|
|
3763
|
+
}
|
|
3764
|
+
};
|
|
3765
|
+
var AlipayProtocolError = class extends MoltsPayError {
|
|
3766
|
+
constructor(message) {
|
|
3767
|
+
super("ALIPAY_PROTOCOL", message);
|
|
3768
|
+
this.name = "AlipayProtocolError";
|
|
3769
|
+
}
|
|
3770
|
+
};
|
|
3771
|
+
var UnsupportedRailError = class extends MoltsPayError {
|
|
3772
|
+
constructor(rail, message) {
|
|
3773
|
+
super("UNSUPPORTED_RAIL", message ?? `Rail not supported by server: ${rail}`);
|
|
3774
|
+
this.rail = rail;
|
|
3775
|
+
this.name = "UnsupportedRailError";
|
|
3776
|
+
}
|
|
3777
|
+
};
|
|
3778
|
+
|
|
3779
|
+
// src/client/alipay/install.ts
|
|
3780
|
+
var execFileAsync = promisify(execFile);
|
|
3781
|
+
var MIN_CLI_VERSION = "0.3.15";
|
|
3782
|
+
function semverLt(a, b) {
|
|
3783
|
+
const pa = a.split(".").map((n) => parseInt(n, 10));
|
|
3784
|
+
const pb = b.split(".").map((n) => parseInt(n, 10));
|
|
3785
|
+
for (let i = 0; i < 3; i++) {
|
|
3786
|
+
const x = pa[i] ?? 0;
|
|
3787
|
+
const y = pb[i] ?? 0;
|
|
3788
|
+
if (x !== y) return x < y;
|
|
3789
|
+
}
|
|
3790
|
+
return false;
|
|
3791
|
+
}
|
|
3792
|
+
function parseVersion(stdout) {
|
|
3793
|
+
return stdout.match(/v?(\d+\.\d+\.\d+)/)?.[1] ?? null;
|
|
3794
|
+
}
|
|
3795
|
+
var defaultGetVersion = async () => {
|
|
3796
|
+
const { stdout } = await execFileAsync("alipay-bot", ["--version"]);
|
|
3797
|
+
return stdout;
|
|
3798
|
+
};
|
|
3799
|
+
async function ensureCliUncached(getVersion) {
|
|
3800
|
+
let stdout;
|
|
3801
|
+
try {
|
|
3802
|
+
stdout = await getVersion();
|
|
3803
|
+
} catch (e) {
|
|
3804
|
+
if (e?.code === "ENOENT") {
|
|
3805
|
+
throw new AlipayCliNotFoundError(
|
|
3806
|
+
"alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
|
|
3807
|
+
);
|
|
3808
|
+
}
|
|
3809
|
+
throw e;
|
|
3810
|
+
}
|
|
3811
|
+
const version = parseVersion(stdout);
|
|
3812
|
+
if (!version || semverLt(version, MIN_CLI_VERSION)) {
|
|
3813
|
+
throw new AlipayCliVersionError(
|
|
3814
|
+
`alipay-bot ${version ?? "?"} found, need \u2265 ${MIN_CLI_VERSION}. Run: npx -y @alipay/agent-payment@latest update`
|
|
3815
|
+
);
|
|
3816
|
+
}
|
|
3817
|
+
return version;
|
|
3818
|
+
}
|
|
3819
|
+
var cachedVersion = null;
|
|
3820
|
+
var inflight = null;
|
|
3821
|
+
async function ensureCli(getVersion = defaultGetVersion) {
|
|
3822
|
+
if (getVersion !== defaultGetVersion) {
|
|
3823
|
+
return ensureCliUncached(getVersion);
|
|
3824
|
+
}
|
|
3825
|
+
if (cachedVersion) return cachedVersion;
|
|
3826
|
+
if (!inflight) {
|
|
3827
|
+
inflight = ensureCliUncached(getVersion).then((v) => {
|
|
3828
|
+
cachedVersion = v;
|
|
3829
|
+
return v;
|
|
3830
|
+
}).finally(() => {
|
|
3831
|
+
inflight = null;
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3834
|
+
return inflight;
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
// src/client/alipay/poll.ts
|
|
3838
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
3839
|
+
var POLL_MAX_INFLIGHT = 2;
|
|
3840
|
+
function resolveMaxInflight(override) {
|
|
3841
|
+
if (override !== void 0) return Math.max(1, Math.floor(override));
|
|
3842
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_MAX_INFLIGHT;
|
|
3843
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
3844
|
+
const n = Number(raw);
|
|
3845
|
+
if (Number.isFinite(n) && n >= 1) return Math.floor(n);
|
|
3846
|
+
}
|
|
3847
|
+
return POLL_MAX_INFLIGHT;
|
|
3848
|
+
}
|
|
3849
|
+
function resolveLaunchGap(override) {
|
|
3850
|
+
if (override !== void 0) return Math.max(0, override);
|
|
3851
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_LAUNCH_MS;
|
|
3852
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
3853
|
+
const n = Number(raw);
|
|
3854
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
3855
|
+
}
|
|
3856
|
+
return POLL_INTERVAL_MS;
|
|
3857
|
+
}
|
|
3858
|
+
function parseStatus(lines) {
|
|
3859
|
+
const raw = lines.join("\n").trim();
|
|
3860
|
+
try {
|
|
3861
|
+
const json = JSON.parse(raw);
|
|
3862
|
+
if (json && typeof json === "object") {
|
|
3863
|
+
if (typeof json.success === "boolean") {
|
|
3864
|
+
if (json.success) return "paid";
|
|
3865
|
+
const err = String(json.errorCode ?? "").toUpperCase();
|
|
3866
|
+
if (/UNPAID|WAIT|PENDING|PROCESS|NOTPAY/.test(err)) return "pending";
|
|
3867
|
+
if (/CLOSED|CANCEL|FAIL|REJECT|REFUSE|TIMEOUT|EXPIRE/.test(err)) return "rejected";
|
|
3868
|
+
return "pending";
|
|
3869
|
+
}
|
|
3870
|
+
if (typeof json.code !== "undefined") {
|
|
3871
|
+
if (Number(json.code) === 200) return "paid";
|
|
3872
|
+
const msg = `${json.message ?? ""}${json.reason ?? ""}`;
|
|
3873
|
+
if (/关闭|失败|拒绝|取消|超时|已撤销/.test(msg)) return "rejected";
|
|
3874
|
+
return "pending";
|
|
3875
|
+
}
|
|
3876
|
+
if (typeof json.body === "string") {
|
|
3877
|
+
return classifyReportText(json.body);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
} catch {
|
|
3881
|
+
}
|
|
3882
|
+
return classifyReportText(raw);
|
|
3883
|
+
}
|
|
3884
|
+
function classifyReportText(s) {
|
|
3885
|
+
const text = s.toUpperCase();
|
|
3886
|
+
if (/查询支付状态成功并获取资源/.test(s) || /资源响应状态[^0-9\n]{0,8}200/.test(s) || /TRADE_SUCCESS|TRADE_FINISHED|"STATUS":\s*"FULFILLED"/.test(text)) return "paid";
|
|
3887
|
+
if (/TRADE_CLOSED|REJECTED|REFUSE|CANCEL/.test(text) || /交易关闭|支付失败|已拒绝|已取消|已撤销|交易超时/.test(s)) return "rejected";
|
|
3888
|
+
if (/WAIT_BUYER_PAY|UNPAID|PENDING|WAITING/.test(text) || /交易未支付|等待付款|待支付|未支付/.test(s)) return "pending";
|
|
3889
|
+
return "unknown";
|
|
3890
|
+
}
|
|
3891
|
+
function defaultSleep(ms, signal) {
|
|
3892
|
+
return new Promise((resolve2, reject) => {
|
|
3893
|
+
if (signal?.aborted) return reject(new Error("aborted"));
|
|
3894
|
+
const t = setTimeout(resolve2, ms);
|
|
3895
|
+
signal?.addEventListener(
|
|
3896
|
+
"abort",
|
|
3897
|
+
() => {
|
|
3898
|
+
clearTimeout(t);
|
|
3899
|
+
reject(new Error("aborted"));
|
|
3900
|
+
},
|
|
3901
|
+
{ once: true }
|
|
3902
|
+
);
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3905
|
+
var LAUNCH = /* @__PURE__ */ Symbol("launch");
|
|
3906
|
+
async function pollUntil(tradeNo, resourceUrl, opts) {
|
|
3907
|
+
const runner = opts.runner ?? runCli;
|
|
3908
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
3909
|
+
const now = opts.now ?? Date.now;
|
|
3910
|
+
const maxInflight = resolveMaxInflight(opts.maxInflight);
|
|
3911
|
+
const launchGap = resolveLaunchGap(opts.launchIntervalMs);
|
|
3912
|
+
const args = [
|
|
3913
|
+
"402-query-payment-status",
|
|
3914
|
+
"-t",
|
|
3915
|
+
tradeNo,
|
|
3916
|
+
"-r",
|
|
3917
|
+
resourceUrl,
|
|
3918
|
+
...opts.method ? ["-m", opts.method] : [],
|
|
3919
|
+
...opts.data ? ["-d", opts.data] : []
|
|
3920
|
+
];
|
|
3921
|
+
const internal = new AbortController();
|
|
3922
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, internal.signal]) : internal.signal;
|
|
3923
|
+
let tick = 0;
|
|
3924
|
+
let lastLaunch = -Infinity;
|
|
3925
|
+
const inflight2 = /* @__PURE__ */ new Set();
|
|
3926
|
+
const launch = () => {
|
|
3927
|
+
tick += 1;
|
|
3928
|
+
const myTick = tick;
|
|
3929
|
+
lastLaunch = now();
|
|
3930
|
+
const run = (async () => {
|
|
3931
|
+
const { lines } = await runner(args, {
|
|
3932
|
+
signal,
|
|
3933
|
+
step: "query-payment-status",
|
|
3934
|
+
flow: tradeNo
|
|
3935
|
+
});
|
|
3936
|
+
const status = parseStatus(lines);
|
|
3937
|
+
alipayLog.debug("poll.tick", { flow: tradeNo, tick: myTick, status });
|
|
3938
|
+
return { status, lines };
|
|
3939
|
+
})();
|
|
3940
|
+
const tracked = run.finally(() => {
|
|
3941
|
+
inflight2.delete(tracked);
|
|
3942
|
+
});
|
|
3943
|
+
inflight2.add(tracked);
|
|
3944
|
+
};
|
|
3945
|
+
try {
|
|
3946
|
+
for (; ; ) {
|
|
3947
|
+
if (opts.signal?.aborted) throw new Error("aborted");
|
|
3948
|
+
const expired = now() >= opts.deadline;
|
|
3949
|
+
if (expired && inflight2.size === 0) {
|
|
3950
|
+
throw new AlipayPaymentTimeoutError(
|
|
3951
|
+
`Payment ${tradeNo} not completed before pay_before deadline`
|
|
3952
|
+
);
|
|
3953
|
+
}
|
|
3954
|
+
while (!expired && inflight2.size < maxInflight && now() - lastLaunch >= launchGap) {
|
|
3955
|
+
launch();
|
|
3956
|
+
}
|
|
3957
|
+
const waiters = [...inflight2];
|
|
3958
|
+
if (!expired && inflight2.size < maxInflight) {
|
|
3959
|
+
const untilNext = Math.max(0, launchGap - (now() - lastLaunch));
|
|
3960
|
+
waiters.push(sleep(untilNext, signal).then(() => LAUNCH, () => LAUNCH));
|
|
3961
|
+
}
|
|
3962
|
+
const winner = await Promise.race(waiters);
|
|
3963
|
+
if (winner === LAUNCH) continue;
|
|
3964
|
+
if (winner.status === "paid") return { status: "paid", lines: winner.lines };
|
|
3965
|
+
if (winner.status === "rejected") {
|
|
3966
|
+
throw new AlipayPaymentRejectedError(`Payment ${tradeNo} was rejected`);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
} finally {
|
|
3970
|
+
internal.abort();
|
|
3971
|
+
for (const p of inflight2) p.catch(() => void 0);
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
// src/client/alipay/index.ts
|
|
3976
|
+
var TRADE_NO_RE = /^\d{32}$/;
|
|
3977
|
+
function resolveSessionId(explicit, envSession) {
|
|
3978
|
+
return explicit ?? envSession ?? randomUUID();
|
|
3979
|
+
}
|
|
3980
|
+
function assertTradeNo(t) {
|
|
3981
|
+
if (!TRADE_NO_RE.test(t)) {
|
|
3982
|
+
throw new AlipayProtocolError(`invalid tradeNo (expect 32 digits): ${t}`);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
function parseTradeNo(lines) {
|
|
3986
|
+
for (const line of lines) {
|
|
3987
|
+
const labeled = line.match(/trade[_-]?no["'\s:=]+(\d{32})/i);
|
|
3988
|
+
if (labeled) return labeled[1];
|
|
3989
|
+
const bare = line.match(/\b(\d{32})\b/);
|
|
3990
|
+
if (bare) return bare[1];
|
|
3991
|
+
}
|
|
3992
|
+
return null;
|
|
3993
|
+
}
|
|
3994
|
+
function parsePaymentUrl(lines) {
|
|
3995
|
+
let paymentUrl;
|
|
3996
|
+
let shortenUrl;
|
|
3997
|
+
for (const line of lines) {
|
|
3998
|
+
const m = line.match(/(alipays?:\/\/\S+|https?:\/\/\S+)/i);
|
|
3999
|
+
if (!m) continue;
|
|
4000
|
+
const url = m[1].replace(/[)\]`>]+$/, "");
|
|
4001
|
+
if (/short|qr\.alipay|surl|\/s\//i.test(line) && !shortenUrl) shortenUrl = url;
|
|
4002
|
+
else if (!paymentUrl) paymentUrl = url;
|
|
4003
|
+
}
|
|
4004
|
+
if (!paymentUrl && shortenUrl) paymentUrl = shortenUrl;
|
|
4005
|
+
return { paymentUrl, shortenUrl };
|
|
4006
|
+
}
|
|
4007
|
+
function extractMedia(line) {
|
|
4008
|
+
const m = line.match(/^\s*MEDIA:\s*(.+?)\s*$/);
|
|
4009
|
+
return m ? m[1] : null;
|
|
4010
|
+
}
|
|
4011
|
+
function isWalletReady(lines) {
|
|
4012
|
+
const text = lines.join("\n").trim();
|
|
4013
|
+
try {
|
|
4014
|
+
const json = JSON.parse(text);
|
|
4015
|
+
if (json && typeof json.code !== "undefined") return Number(json.code) === 200;
|
|
4016
|
+
} catch {
|
|
4017
|
+
}
|
|
4018
|
+
return !/未开通|未开启|NOT[_\s-]*(OPEN|BOUND|SET)|NEEDS?[_\s-]*SETUP|NO[_\s-]*WALLET/i.test(text);
|
|
4019
|
+
}
|
|
4020
|
+
function extractResourceFromReport(report) {
|
|
4021
|
+
const label = report.search(/资源响应体/);
|
|
4022
|
+
const start = report.indexOf("{", label === -1 ? 0 : label);
|
|
4023
|
+
if (start === -1) return void 0;
|
|
4024
|
+
let depth = 0, inStr = false, esc = false;
|
|
4025
|
+
for (let i = start; i < report.length; i++) {
|
|
4026
|
+
const ch = report[i];
|
|
4027
|
+
if (inStr) {
|
|
4028
|
+
if (esc) esc = false;
|
|
4029
|
+
else if (ch === "\\") esc = true;
|
|
4030
|
+
else if (ch === '"') inStr = false;
|
|
4031
|
+
continue;
|
|
4032
|
+
}
|
|
4033
|
+
if (ch === '"') inStr = true;
|
|
4034
|
+
else if (ch === "{") depth++;
|
|
4035
|
+
else if (ch === "}" && --depth === 0) {
|
|
4036
|
+
const slice = report.slice(start, i + 1);
|
|
4037
|
+
try {
|
|
4038
|
+
const obj = JSON.parse(slice);
|
|
4039
|
+
const r = obj?.resourceResponse ?? obj?.result ?? obj?.data ?? obj?.body ?? obj;
|
|
4040
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
4041
|
+
} catch {
|
|
4042
|
+
return slice;
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
return void 0;
|
|
4047
|
+
}
|
|
4048
|
+
function extractBody(lines) {
|
|
4049
|
+
const text = lines.join("\n").trim();
|
|
4050
|
+
try {
|
|
4051
|
+
const json = JSON.parse(text);
|
|
4052
|
+
if (json && typeof json === "object") {
|
|
4053
|
+
if (json.resourceResponse !== void 0 && json.resourceResponse !== null) {
|
|
4054
|
+
const rr = json.resourceResponse;
|
|
4055
|
+
const r = typeof rr === "object" ? rr.result ?? rr.data ?? rr.body ?? rr : rr;
|
|
4056
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
4057
|
+
}
|
|
4058
|
+
if (typeof json.body === "string") {
|
|
4059
|
+
const inner = extractResourceFromReport(json.body);
|
|
4060
|
+
return inner ?? json.body;
|
|
4061
|
+
}
|
|
4062
|
+
const body = json.data ?? json.result ?? json.body ?? json.resource;
|
|
4063
|
+
if (body !== void 0) return typeof body === "string" ? body : JSON.stringify(body);
|
|
4064
|
+
return text;
|
|
4065
|
+
}
|
|
4066
|
+
} catch {
|
|
4067
|
+
}
|
|
4068
|
+
const idx = lines.findIndex((l) => /^\s*BODY:/.test(l));
|
|
4069
|
+
if (idx !== -1) {
|
|
4070
|
+
const first = lines[idx].replace(/^\s*BODY:\s*/, "");
|
|
4071
|
+
return [first, ...lines.slice(idx + 1)].join("\n").trim();
|
|
4072
|
+
}
|
|
4073
|
+
return lines.filter((l) => !/^\s*(MEDIA|STATUS|INFO):/.test(l)).join("\n").trim();
|
|
4074
|
+
}
|
|
4075
|
+
var DEFAULT_WALLET_TTL_MS = 10 * 60 * 1e3;
|
|
4076
|
+
function resolveWalletTtlMs() {
|
|
4077
|
+
const raw = process.env.MOLTSPAY_ALIPAY_WALLET_TTL_MS;
|
|
4078
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_WALLET_TTL_MS;
|
|
4079
|
+
const n = Number(raw);
|
|
4080
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_WALLET_TTL_MS;
|
|
4081
|
+
}
|
|
4082
|
+
var walletReadyUntil = /* @__PURE__ */ new Map();
|
|
4083
|
+
function resetWalletCache() {
|
|
4084
|
+
walletReadyUntil.clear();
|
|
4085
|
+
}
|
|
4086
|
+
var DEFAULT_INTENT_TTL_MS = 10 * 60 * 1e3;
|
|
4087
|
+
function resolveIntentTtlMs() {
|
|
4088
|
+
const raw = process.env.MOLTSPAY_ALIPAY_INTENT_TTL_MS;
|
|
4089
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_INTENT_TTL_MS;
|
|
4090
|
+
const n = Number(raw);
|
|
4091
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTENT_TTL_MS;
|
|
4092
|
+
}
|
|
4093
|
+
var intentDoneUntil = /* @__PURE__ */ new Map();
|
|
4094
|
+
function resetIntentCache() {
|
|
4095
|
+
intentDoneUntil.clear();
|
|
4096
|
+
}
|
|
4097
|
+
var AlipayClient = class {
|
|
4098
|
+
sessionId;
|
|
4099
|
+
configDir;
|
|
4100
|
+
framework;
|
|
4101
|
+
runner;
|
|
4102
|
+
getVersion;
|
|
4103
|
+
now;
|
|
4104
|
+
/** Only the default runner may use the process-level wallet cache. */
|
|
4105
|
+
walletCacheable;
|
|
4106
|
+
/** Only the default runner may use the process-level payment-intent cache. */
|
|
4107
|
+
intentCacheable;
|
|
4108
|
+
constructor(opts = {}) {
|
|
4109
|
+
this.sessionId = resolveSessionId(opts.sessionId, process.env.AIPAY_SESSION_ID);
|
|
4110
|
+
this.configDir = opts.configDir ?? join4(homedir2(), ".moltspay");
|
|
4111
|
+
this.framework = opts.framework ?? process.env.AIPAY_FRAMEWORK ?? "openclaw";
|
|
4112
|
+
this.runner = opts.runner ?? runCli;
|
|
4113
|
+
this.getVersion = opts.getVersion;
|
|
4114
|
+
this.now = opts.now ?? Date.now;
|
|
4115
|
+
this.walletCacheable = opts.cacheWallet ?? !opts.runner;
|
|
4116
|
+
this.intentCacheable = opts.cacheIntent ?? !opts.runner;
|
|
4117
|
+
}
|
|
4118
|
+
/**
|
|
4119
|
+
* Throws NeedsWalletSetupError unless alipay-bot reports an opened wallet.
|
|
4120
|
+
*
|
|
4121
|
+
* Skips the ~22s `check-wallet` spawn entirely when a prior call cached a
|
|
4122
|
+
* "ready" verdict within the TTL (see {@link DEFAULT_WALLET_TTL_MS}).
|
|
4123
|
+
*/
|
|
4124
|
+
async checkWallet(signal, flow) {
|
|
4125
|
+
const ttlMs = resolveWalletTtlMs();
|
|
4126
|
+
const cacheKey = `${this.configDir}::${this.framework}`;
|
|
4127
|
+
const useCache = this.walletCacheable && ttlMs > 0;
|
|
4128
|
+
if (useCache) {
|
|
4129
|
+
const until = walletReadyUntil.get(cacheKey);
|
|
4130
|
+
if (until !== void 0 && this.now() < until) {
|
|
4131
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: true, ttlMsLeft: until - this.now() });
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
const { lines } = await this.runner(["check-wallet"], { signal, step: "check-wallet", flow });
|
|
4136
|
+
if (!isWalletReady(lines)) {
|
|
4137
|
+
if (this.walletCacheable) walletReadyUntil.delete(cacheKey);
|
|
4138
|
+
throw new NeedsWalletSetupError(
|
|
4139
|
+
"Alipay wallet not opened. Run: moltspay alipay apply (then: moltspay alipay bind)"
|
|
4140
|
+
);
|
|
4141
|
+
}
|
|
4142
|
+
if (useCache) {
|
|
4143
|
+
walletReadyUntil.set(cacheKey, this.now() + ttlMs);
|
|
4144
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: false, cachedForMs: ttlMs });
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
/** Run the full 8-step flow and resolve with the resource body. */
|
|
4148
|
+
async pay402(opts) {
|
|
4149
|
+
const { resourceUrl, requirement, signal } = opts;
|
|
4150
|
+
const extra = requirement.extra ?? {};
|
|
4151
|
+
const paymentNeededHeader = String(extra.payment_needed_header ?? "");
|
|
4152
|
+
if (!paymentNeededHeader) {
|
|
4153
|
+
throw new AlipayProtocolError("alipay requirement missing extra.payment_needed_header");
|
|
4154
|
+
}
|
|
4155
|
+
const flow = this.sessionId;
|
|
4156
|
+
const flowStart = this.now();
|
|
4157
|
+
alipayLog.info("flow.start", { flow, resource: resourceUrl });
|
|
4158
|
+
const media = [];
|
|
4159
|
+
const onLine = (line) => {
|
|
4160
|
+
const m = extractMedia(line);
|
|
4161
|
+
if (m) media.push(m);
|
|
4162
|
+
else opts.onLine?.(line);
|
|
4163
|
+
};
|
|
4164
|
+
const intentSummary = opts.intentSummary?.trim() || `\u652F\u4ED8 ${requirement.amount ?? ""} ${requirement.asset ?? "CNY"}`.trim();
|
|
4165
|
+
await timeStep("ensure-cli", flow, () => ensureCli(this.getVersion));
|
|
4166
|
+
const intentTtlMs = resolveIntentTtlMs();
|
|
4167
|
+
const intentKey = `${this.configDir}::${this.framework}`;
|
|
4168
|
+
const useIntentCache = this.intentCacheable && intentTtlMs > 0;
|
|
4169
|
+
const intentUntil = useIntentCache ? intentDoneUntil.get(intentKey) : void 0;
|
|
4170
|
+
if (intentUntil !== void 0 && this.now() < intentUntil) {
|
|
4171
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: true, ttlMsLeft: intentUntil - this.now() });
|
|
4172
|
+
} else {
|
|
4173
|
+
await this.runner(
|
|
4174
|
+
[
|
|
4175
|
+
"payment-intent",
|
|
4176
|
+
"--session-id",
|
|
4177
|
+
this.sessionId,
|
|
4178
|
+
"--intent-summary",
|
|
4179
|
+
intentSummary,
|
|
4180
|
+
"--framework",
|
|
4181
|
+
this.framework
|
|
4182
|
+
],
|
|
4183
|
+
{ onLine, signal, step: "payment-intent", flow }
|
|
4184
|
+
);
|
|
4185
|
+
if (useIntentCache) {
|
|
4186
|
+
intentDoneUntil.set(intentKey, this.now() + intentTtlMs);
|
|
4187
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: false, cachedForMs: intentTtlMs });
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
await this.checkWallet(signal, flow);
|
|
4191
|
+
const reqId = String(extra.out_trade_no ?? randomUUID());
|
|
4192
|
+
const dir = join4(this.configDir, "alipay");
|
|
4193
|
+
await mkdir(dir, { recursive: true });
|
|
4194
|
+
const challengeFile = join4(dir, `402_${reqId}.txt`);
|
|
4195
|
+
await writeFile(challengeFile, paymentNeededHeader, "utf-8");
|
|
4196
|
+
const payArgs = [
|
|
4197
|
+
"402-buyer-pay",
|
|
4198
|
+
"-f",
|
|
4199
|
+
challengeFile,
|
|
4200
|
+
"-r",
|
|
4201
|
+
resourceUrl,
|
|
4202
|
+
"-s",
|
|
4203
|
+
this.sessionId,
|
|
4204
|
+
"-i",
|
|
4205
|
+
intentSummary,
|
|
4206
|
+
"-w",
|
|
4207
|
+
this.framework
|
|
4208
|
+
];
|
|
4209
|
+
if (opts.method) payArgs.push("-m", opts.method);
|
|
4210
|
+
if (opts.data) payArgs.push("-d", opts.data);
|
|
4211
|
+
const payRun = await this.runner(payArgs, { onLine, signal, step: "402-buyer-pay", flow });
|
|
4212
|
+
const tradeNo = parseTradeNo(payRun.lines);
|
|
4213
|
+
if (!tradeNo) {
|
|
4214
|
+
throw new AlipayProtocolError("402-buyer-pay did not return a tradeNo");
|
|
4215
|
+
}
|
|
4216
|
+
assertTradeNo(tradeNo);
|
|
4217
|
+
const { paymentUrl, shortenUrl } = parsePaymentUrl(payRun.lines);
|
|
4218
|
+
if (paymentUrl) {
|
|
4219
|
+
alipayLog.info("flow.pending", { flow, tradeNo, ms: this.now() - flowStart });
|
|
4220
|
+
opts.onPaymentPending?.({ paymentUrl, shortenUrl, tradeNo });
|
|
4221
|
+
}
|
|
4222
|
+
const pendingAt = this.now();
|
|
4223
|
+
const windowMs = (requirement.maxTimeoutSeconds ?? 30 * 60) * 1e3;
|
|
4224
|
+
const deadline = this.now() + (opts.timeoutMs ?? windowMs);
|
|
4225
|
+
const poll = await pollUntil(tradeNo, resourceUrl, {
|
|
4226
|
+
// No onLine: the status-poll output embeds the resource and must not reach
|
|
4227
|
+
// the log stream — the body is surfaced via the return value below (§9.3).
|
|
4228
|
+
deadline,
|
|
4229
|
+
signal,
|
|
4230
|
+
runner: this.runner,
|
|
4231
|
+
now: this.now,
|
|
4232
|
+
// Re-fetch the resource the same way it was paid (POST + body), else the
|
|
4233
|
+
// status poll defaults to GET and 404s on a POST-only `/execute`.
|
|
4234
|
+
method: opts.method,
|
|
4235
|
+
data: opts.data
|
|
4236
|
+
});
|
|
4237
|
+
alipayLog.info("flow.settled", { flow, tradeNo, ms: this.now() - pendingAt });
|
|
4238
|
+
const body = extractBody(poll.lines);
|
|
4239
|
+
void this.runner(["402-buyer-fulfillment-ack", "-t", tradeNo], {
|
|
4240
|
+
onLine,
|
|
4241
|
+
signal,
|
|
4242
|
+
step: "402-buyer-fulfillment-ack",
|
|
4243
|
+
flow
|
|
4244
|
+
}).catch(() => void 0);
|
|
4245
|
+
return { body, payment: { tradeNo, outTradeNo: reqId }, media };
|
|
4246
|
+
}
|
|
4247
|
+
};
|
|
4248
|
+
|
|
4249
|
+
// src/client/alipay/router.ts
|
|
4250
|
+
var ALIPAY_RAIL = "alipay";
|
|
4251
|
+
function railOf(req) {
|
|
4252
|
+
if (req.scheme === "alipay-aipay" || req.network === ALIPAY_RAIL) return ALIPAY_RAIL;
|
|
4253
|
+
return networkToChainName(req.network) ?? req.network;
|
|
4254
|
+
}
|
|
4255
|
+
function findRail(accepts, rail) {
|
|
4256
|
+
const requirement = accepts.find((r) => railOf(r) === rail);
|
|
4257
|
+
return requirement ? { rail, requirement } : null;
|
|
4258
|
+
}
|
|
4259
|
+
function selectRail(input) {
|
|
4260
|
+
const { serverAccepts, explicitRail, preference, availability } = input;
|
|
4261
|
+
if (!serverAccepts || serverAccepts.length === 0) {
|
|
4262
|
+
throw new UnsupportedRailError(explicitRail ?? "unknown", "Server offered no payment options");
|
|
4263
|
+
}
|
|
4264
|
+
if (explicitRail) {
|
|
4265
|
+
const hit = findRail(serverAccepts, explicitRail);
|
|
4266
|
+
if (!hit) {
|
|
4267
|
+
const offered = serverAccepts.map(railOf);
|
|
4268
|
+
throw new UnsupportedRailError(
|
|
4269
|
+
explicitRail,
|
|
4270
|
+
`Server doesn't accept rail '${explicitRail}'. Offered: ${offered.join(", ")}`
|
|
4271
|
+
);
|
|
4272
|
+
}
|
|
4273
|
+
return hit;
|
|
4274
|
+
}
|
|
4275
|
+
if (preference) {
|
|
4276
|
+
for (const pref of preference) {
|
|
4277
|
+
const hit = findRail(serverAccepts, pref);
|
|
4278
|
+
if (hit) return hit;
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
if (availability?.evmReady) {
|
|
4282
|
+
const evm = serverAccepts.find((r) => railOf(r) !== ALIPAY_RAIL);
|
|
4283
|
+
if (evm) return { rail: railOf(evm), requirement: evm };
|
|
4284
|
+
}
|
|
4285
|
+
if (availability?.alipayReady) {
|
|
4286
|
+
const hit = findRail(serverAccepts, ALIPAY_RAIL);
|
|
4287
|
+
if (hit) return hit;
|
|
4288
|
+
}
|
|
4289
|
+
return { rail: railOf(serverAccepts[0]), requirement: serverAccepts[0] };
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
// src/client/node/index.ts
|
|
2653
4293
|
var DEFAULT_CONFIG = {
|
|
2654
4294
|
chain: "base",
|
|
2655
4295
|
limits: {
|
|
@@ -2662,15 +4302,24 @@ var MoltsPayClient = class {
|
|
|
2662
4302
|
config;
|
|
2663
4303
|
walletData = null;
|
|
2664
4304
|
wallet = null;
|
|
4305
|
+
signer = null;
|
|
2665
4306
|
todaySpending = 0;
|
|
2666
4307
|
lastSpendingReset = 0;
|
|
4308
|
+
railPreference;
|
|
4309
|
+
alipaySessionId;
|
|
2667
4310
|
constructor(options = {}) {
|
|
2668
|
-
this.configDir = options.configDir ||
|
|
4311
|
+
this.configDir = options.configDir || join5(homedir3(), ".moltspay");
|
|
2669
4312
|
this.config = this.loadConfig();
|
|
4313
|
+
this.railPreference = options.railPreference ?? this.config.railPreference;
|
|
4314
|
+
this.alipaySessionId = options.alipaySessionId;
|
|
2670
4315
|
this.walletData = this.loadWallet();
|
|
2671
4316
|
this.loadSpending();
|
|
2672
4317
|
if (this.walletData) {
|
|
2673
|
-
this.wallet = new
|
|
4318
|
+
this.wallet = new Wallet2(this.walletData.privateKey);
|
|
4319
|
+
const configDir = this.configDir;
|
|
4320
|
+
this.signer = new NodeSigner(this.wallet, {
|
|
4321
|
+
getSolanaKeypair: () => loadSolanaWallet(configDir)
|
|
4322
|
+
});
|
|
2674
4323
|
}
|
|
2675
4324
|
}
|
|
2676
4325
|
/**
|
|
@@ -2740,6 +4389,9 @@ var MoltsPayClient = class {
|
|
|
2740
4389
|
* @param options - Payment options (token selection)
|
|
2741
4390
|
*/
|
|
2742
4391
|
async pay(serverUrl, service, params, options = {}) {
|
|
4392
|
+
if (options.rail === ALIPAY_RAIL) {
|
|
4393
|
+
return this.payViaAlipay(serverUrl, service, params, options);
|
|
4394
|
+
}
|
|
2743
4395
|
if (!this.wallet || !this.walletData) {
|
|
2744
4396
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
2745
4397
|
}
|
|
@@ -2798,20 +4450,6 @@ var MoltsPayClient = class {
|
|
|
2798
4450
|
} catch {
|
|
2799
4451
|
throw new Error("Invalid x-payment-required header");
|
|
2800
4452
|
}
|
|
2801
|
-
const networkToChainName = (network2) => {
|
|
2802
|
-
if (network2 === "solana:mainnet") return "solana";
|
|
2803
|
-
if (network2 === "solana:devnet") return "solana_devnet";
|
|
2804
|
-
const match = network2.match(/^eip155:(\d+)$/);
|
|
2805
|
-
if (!match) return null;
|
|
2806
|
-
const chainId = parseInt(match[1]);
|
|
2807
|
-
if (chainId === 8453) return "base";
|
|
2808
|
-
if (chainId === 137) return "polygon";
|
|
2809
|
-
if (chainId === 84532) return "base_sepolia";
|
|
2810
|
-
if (chainId === 42431) return "tempo_moderato";
|
|
2811
|
-
if (chainId === 56) return "bnb";
|
|
2812
|
-
if (chainId === 97) return "bnb_testnet";
|
|
2813
|
-
return null;
|
|
2814
|
-
};
|
|
2815
4453
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
2816
4454
|
const userSpecifiedChain = options.chain;
|
|
2817
4455
|
let selectedChain;
|
|
@@ -2945,6 +4583,76 @@ Please specify: --chain <chain_name>`
|
|
|
2945
4583
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
2946
4584
|
return result.result || result;
|
|
2947
4585
|
}
|
|
4586
|
+
/**
|
|
4587
|
+
* Pay for a service over the Alipay fiat rail (2.0.0).
|
|
4588
|
+
*
|
|
4589
|
+
* Unlike the crypto path this needs no EVM wallet — it shells out to
|
|
4590
|
+
* alipay-bot via {@link AlipayClient}. Flow: hit the resource with no
|
|
4591
|
+
* payment to get the 402 challenge, confirm the server actually offers the
|
|
4592
|
+
* alipay rail (selectRail), then run the 8-step state machine and return the
|
|
4593
|
+
* resource body.
|
|
4594
|
+
*/
|
|
4595
|
+
async payViaAlipay(serverUrl, service, params, options) {
|
|
4596
|
+
const flow = this.alipaySessionId;
|
|
4597
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
4598
|
+
try {
|
|
4599
|
+
const services = await timeStep(
|
|
4600
|
+
"discover-services",
|
|
4601
|
+
flow,
|
|
4602
|
+
() => this.getServices(serverUrl)
|
|
4603
|
+
);
|
|
4604
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
4605
|
+
if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
4606
|
+
} catch {
|
|
4607
|
+
}
|
|
4608
|
+
const requestBody = options.rawData ? { service, ...params } : { service, params };
|
|
4609
|
+
const bodyJson = JSON.stringify(requestBody);
|
|
4610
|
+
const res = await timeStep(
|
|
4611
|
+
"challenge-402",
|
|
4612
|
+
flow,
|
|
4613
|
+
() => fetch(executeUrl, {
|
|
4614
|
+
method: "POST",
|
|
4615
|
+
headers: { "Content-Type": "application/json" },
|
|
4616
|
+
body: bodyJson
|
|
4617
|
+
})
|
|
4618
|
+
);
|
|
4619
|
+
if (res.status !== 402) {
|
|
4620
|
+
const data = await res.json().catch(() => ({}));
|
|
4621
|
+
if (res.ok && data.result) return data.result;
|
|
4622
|
+
throw new Error(data.error || `Expected 402, got ${res.status}`);
|
|
4623
|
+
}
|
|
4624
|
+
const header = res.headers.get(PAYMENT_REQUIRED_HEADER2);
|
|
4625
|
+
if (!header) throw new Error("Missing x-payment-required header on 402");
|
|
4626
|
+
const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
|
|
4627
|
+
const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
|
|
4628
|
+
const { requirement } = selectRail({
|
|
4629
|
+
serverAccepts: accepts,
|
|
4630
|
+
explicitRail: ALIPAY_RAIL,
|
|
4631
|
+
preference: this.railPreference,
|
|
4632
|
+
availability: { evmReady: this.isInitialized }
|
|
4633
|
+
});
|
|
4634
|
+
const onLine = options.onLine ?? ((line) => process.stdout.write(line + "\n"));
|
|
4635
|
+
const alipay = new AlipayClient({
|
|
4636
|
+
sessionId: this.alipaySessionId,
|
|
4637
|
+
configDir: this.configDir
|
|
4638
|
+
});
|
|
4639
|
+
const result = await alipay.pay402({
|
|
4640
|
+
resourceUrl: executeUrl,
|
|
4641
|
+
requirement,
|
|
4642
|
+
method: "POST",
|
|
4643
|
+
data: bodyJson,
|
|
4644
|
+
onLine,
|
|
4645
|
+
onPaymentPending: options.onPaymentPending,
|
|
4646
|
+
timeoutMs: options.timeoutMs,
|
|
4647
|
+
signal: options.signal
|
|
4648
|
+
});
|
|
4649
|
+
try {
|
|
4650
|
+
const json = JSON.parse(result.body);
|
|
4651
|
+
return json.result ?? json;
|
|
4652
|
+
} catch {
|
|
4653
|
+
return { body: result.body, payment: result.payment, media: result.media };
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
2948
4656
|
/**
|
|
2949
4657
|
* Handle MPP (Machine Payments Protocol) payment flow
|
|
2950
4658
|
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
@@ -3040,14 +4748,14 @@ Please specify: --chain <chain_name>`
|
|
|
3040
4748
|
async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
|
|
3041
4749
|
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
3042
4750
|
const tokenConfig = chain.tokens[token];
|
|
3043
|
-
const provider = new
|
|
4751
|
+
const provider = new ethers3.JsonRpcProvider(chain.rpc);
|
|
3044
4752
|
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
3045
4753
|
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
3046
4754
|
if (allowance < amountWeiCheck) {
|
|
3047
4755
|
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
3048
|
-
const minGasBalance =
|
|
4756
|
+
const minGasBalance = ethers3.parseEther("0.0005");
|
|
3049
4757
|
if (nativeBalance < minGasBalance) {
|
|
3050
|
-
const nativeBNB = parseFloat(
|
|
4758
|
+
const nativeBNB = parseFloat(ethers3.formatEther(nativeBalance)).toFixed(4);
|
|
3051
4759
|
const isTestnet = chainName === "bnb_testnet";
|
|
3052
4760
|
if (isTestnet) {
|
|
3053
4761
|
throw new Error(
|
|
@@ -3081,35 +4789,21 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3081
4789
|
);
|
|
3082
4790
|
}
|
|
3083
4791
|
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
3084
|
-
const
|
|
4792
|
+
const intentNonce = Date.now();
|
|
4793
|
+
const intentDeadline = Date.now() + 36e5;
|
|
4794
|
+
const envelope = buildBnbIntentTypedData({
|
|
3085
4795
|
from: this.wallet.address,
|
|
3086
4796
|
to,
|
|
3087
4797
|
amount: amountWei,
|
|
3088
|
-
|
|
4798
|
+
tokenAddress: tokenConfig.address,
|
|
3089
4799
|
service,
|
|
3090
|
-
nonce:
|
|
3091
|
-
|
|
3092
|
-
deadline: Date.now() + 36e5
|
|
3093
|
-
// 1 hour
|
|
3094
|
-
};
|
|
3095
|
-
const domain = {
|
|
3096
|
-
name: "MoltsPay",
|
|
3097
|
-
version: "1",
|
|
4800
|
+
nonce: intentNonce,
|
|
4801
|
+
deadline: intentDeadline,
|
|
3098
4802
|
chainId: chain.chainId
|
|
3099
|
-
};
|
|
3100
|
-
const types = {
|
|
3101
|
-
PaymentIntent: [
|
|
3102
|
-
{ name: "from", type: "address" },
|
|
3103
|
-
{ name: "to", type: "address" },
|
|
3104
|
-
{ name: "amount", type: "uint256" },
|
|
3105
|
-
{ name: "token", type: "address" },
|
|
3106
|
-
{ name: "service", type: "string" },
|
|
3107
|
-
{ name: "nonce", type: "uint256" },
|
|
3108
|
-
{ name: "deadline", type: "uint256" }
|
|
3109
|
-
]
|
|
3110
|
-
};
|
|
4803
|
+
});
|
|
3111
4804
|
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
3112
|
-
const signature = await this.
|
|
4805
|
+
const signature = await this.signer.signTypedData(envelope);
|
|
4806
|
+
const intent = envelope.message;
|
|
3113
4807
|
const network = `eip155:${chain.chainId}`;
|
|
3114
4808
|
const payload = {
|
|
3115
4809
|
x402Version: 2,
|
|
@@ -3183,12 +4877,11 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3183
4877
|
feePayerPubkey
|
|
3184
4878
|
// Optional fee payer for gasless mode
|
|
3185
4879
|
);
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
}
|
|
3191
|
-
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
4880
|
+
const unsignedBase64 = transaction.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
|
|
4881
|
+
const signedTx = await this.signer.signSolanaTransaction({
|
|
4882
|
+
transactionBase64: unsignedBase64,
|
|
4883
|
+
partialSign: !!feePayerPubkey
|
|
4884
|
+
});
|
|
3192
4885
|
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
3193
4886
|
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
3194
4887
|
const payload = {
|
|
@@ -3235,7 +4928,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3235
4928
|
* Check ERC20 allowance for a spender
|
|
3236
4929
|
*/
|
|
3237
4930
|
async checkAllowance(tokenAddress, spender, provider) {
|
|
3238
|
-
const contract = new
|
|
4931
|
+
const contract = new ethers3.Contract(
|
|
3239
4932
|
tokenAddress,
|
|
3240
4933
|
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
3241
4934
|
provider
|
|
@@ -3246,41 +4939,29 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3246
4939
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
3247
4940
|
* This only signs - no on-chain transaction, no gas needed.
|
|
3248
4941
|
* Supports both USDC and USDT.
|
|
4942
|
+
*
|
|
4943
|
+
* Delegates typed-data construction to `core/eip3009.ts` and the signature
|
|
4944
|
+
* itself to `this.signer`. That way Web Client (Phase 4) can reuse the same
|
|
4945
|
+
* flow with an EIP-1193 signer without duplicating typed-data layout.
|
|
3249
4946
|
*/
|
|
3250
4947
|
async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
|
|
3251
|
-
const validAfter = 0;
|
|
3252
|
-
const validBefore = Math.floor(Date.now() / 1e3) + 3600;
|
|
3253
|
-
const nonce = ethers.hexlify(ethers.randomBytes(32));
|
|
3254
4948
|
const tokenConfig = chain.tokens[token];
|
|
3255
4949
|
const value = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
3256
|
-
const
|
|
4950
|
+
const nonce = ethers3.hexlify(ethers3.randomBytes(32));
|
|
4951
|
+
const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
|
|
4952
|
+
const tokenVersion = domainOverride?.version || "2";
|
|
4953
|
+
const envelope = buildEIP3009TypedData({
|
|
3257
4954
|
from: this.wallet.address,
|
|
3258
4955
|
to,
|
|
3259
4956
|
value,
|
|
3260
|
-
|
|
3261
|
-
validBefore: validBefore.toString(),
|
|
3262
|
-
nonce
|
|
3263
|
-
};
|
|
3264
|
-
const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
|
|
3265
|
-
const tokenVersion = domainOverride?.version || "2";
|
|
3266
|
-
const domain = {
|
|
3267
|
-
name: tokenName,
|
|
3268
|
-
version: tokenVersion,
|
|
4957
|
+
nonce,
|
|
3269
4958
|
chainId: chain.chainId,
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
{ name: "value", type: "uint256" },
|
|
3277
|
-
{ name: "validAfter", type: "uint256" },
|
|
3278
|
-
{ name: "validBefore", type: "uint256" },
|
|
3279
|
-
{ name: "nonce", type: "bytes32" }
|
|
3280
|
-
]
|
|
3281
|
-
};
|
|
3282
|
-
const signature = await this.wallet.signTypedData(domain, types, authorization);
|
|
3283
|
-
return { authorization, signature };
|
|
4959
|
+
tokenAddress: tokenConfig.address,
|
|
4960
|
+
tokenName,
|
|
4961
|
+
tokenVersion
|
|
4962
|
+
});
|
|
4963
|
+
const signature = await this.signer.signTypedData(envelope);
|
|
4964
|
+
return { authorization: envelope.message, signature };
|
|
3284
4965
|
}
|
|
3285
4966
|
/**
|
|
3286
4967
|
* Check spending limits
|
|
@@ -3312,7 +4993,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3312
4993
|
}
|
|
3313
4994
|
// --- Config & Wallet Management ---
|
|
3314
4995
|
loadConfig() {
|
|
3315
|
-
const configPath =
|
|
4996
|
+
const configPath = join5(this.configDir, "config.json");
|
|
3316
4997
|
if (existsSync4(configPath)) {
|
|
3317
4998
|
const content = readFileSync4(configPath, "utf-8");
|
|
3318
4999
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
@@ -3321,14 +5002,14 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3321
5002
|
}
|
|
3322
5003
|
saveConfig() {
|
|
3323
5004
|
mkdirSync2(this.configDir, { recursive: true });
|
|
3324
|
-
const configPath =
|
|
5005
|
+
const configPath = join5(this.configDir, "config.json");
|
|
3325
5006
|
writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
|
|
3326
5007
|
}
|
|
3327
5008
|
/**
|
|
3328
5009
|
* Load spending data from disk
|
|
3329
5010
|
*/
|
|
3330
5011
|
loadSpending() {
|
|
3331
|
-
const spendingPath =
|
|
5012
|
+
const spendingPath = join5(this.configDir, "spending.json");
|
|
3332
5013
|
if (existsSync4(spendingPath)) {
|
|
3333
5014
|
try {
|
|
3334
5015
|
const data = JSON.parse(readFileSync4(spendingPath, "utf-8"));
|
|
@@ -3351,7 +5032,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3351
5032
|
*/
|
|
3352
5033
|
saveSpending() {
|
|
3353
5034
|
mkdirSync2(this.configDir, { recursive: true });
|
|
3354
|
-
const spendingPath =
|
|
5035
|
+
const spendingPath = join5(this.configDir, "spending.json");
|
|
3355
5036
|
const data = {
|
|
3356
5037
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
3357
5038
|
amount: this.todaySpending,
|
|
@@ -3360,7 +5041,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3360
5041
|
writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
|
|
3361
5042
|
}
|
|
3362
5043
|
loadWallet() {
|
|
3363
|
-
const walletPath =
|
|
5044
|
+
const walletPath = join5(this.configDir, "wallet.json");
|
|
3364
5045
|
if (existsSync4(walletPath)) {
|
|
3365
5046
|
if (process.platform !== "win32") {
|
|
3366
5047
|
try {
|
|
@@ -3384,13 +5065,13 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3384
5065
|
*/
|
|
3385
5066
|
static init(configDir, options) {
|
|
3386
5067
|
mkdirSync2(configDir, { recursive: true });
|
|
3387
|
-
const wallet =
|
|
5068
|
+
const wallet = Wallet2.createRandom();
|
|
3388
5069
|
const walletData = {
|
|
3389
5070
|
address: wallet.address,
|
|
3390
5071
|
privateKey: wallet.privateKey,
|
|
3391
5072
|
createdAt: Date.now()
|
|
3392
5073
|
};
|
|
3393
|
-
const walletPath =
|
|
5074
|
+
const walletPath = join5(configDir, "wallet.json");
|
|
3394
5075
|
writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
3395
5076
|
const config = {
|
|
3396
5077
|
chain: options.chain,
|
|
@@ -3399,7 +5080,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3399
5080
|
maxPerDay: options.maxPerDay
|
|
3400
5081
|
}
|
|
3401
5082
|
};
|
|
3402
|
-
const configPath =
|
|
5083
|
+
const configPath = join5(configDir, "config.json");
|
|
3403
5084
|
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
3404
5085
|
return { address: wallet.address, configDir };
|
|
3405
5086
|
}
|
|
@@ -3416,17 +5097,17 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3416
5097
|
} catch {
|
|
3417
5098
|
throw new Error(`Unknown chain: ${this.config.chain}`);
|
|
3418
5099
|
}
|
|
3419
|
-
const provider = new
|
|
5100
|
+
const provider = new ethers3.JsonRpcProvider(chain.rpc);
|
|
3420
5101
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
3421
5102
|
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
3422
5103
|
provider.getBalance(this.wallet.address),
|
|
3423
|
-
new
|
|
3424
|
-
new
|
|
5104
|
+
new ethers3.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
5105
|
+
new ethers3.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
3425
5106
|
]);
|
|
3426
5107
|
return {
|
|
3427
|
-
usdc: parseFloat(
|
|
3428
|
-
usdt: parseFloat(
|
|
3429
|
-
native: parseFloat(
|
|
5108
|
+
usdc: parseFloat(ethers3.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
5109
|
+
usdt: parseFloat(ethers3.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
5110
|
+
native: parseFloat(ethers3.formatEther(nativeBalance))
|
|
3430
5111
|
};
|
|
3431
5112
|
}
|
|
3432
5113
|
/**
|
|
@@ -3449,38 +5130,38 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3449
5130
|
supportedChains.map(async (chainName) => {
|
|
3450
5131
|
try {
|
|
3451
5132
|
const chain = getChain(chainName);
|
|
3452
|
-
const provider = new
|
|
5133
|
+
const provider = new ethers3.JsonRpcProvider(chain.rpc);
|
|
3453
5134
|
if (chainName === "tempo_moderato") {
|
|
3454
5135
|
const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
|
|
3455
5136
|
provider.getBalance(this.wallet.address),
|
|
3456
|
-
new
|
|
3457
|
-
new
|
|
3458
|
-
new
|
|
3459
|
-
new
|
|
5137
|
+
new ethers3.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
5138
|
+
new ethers3.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
5139
|
+
new ethers3.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
5140
|
+
new ethers3.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
3460
5141
|
]);
|
|
3461
5142
|
results[chainName] = {
|
|
3462
|
-
usdc: parseFloat(
|
|
5143
|
+
usdc: parseFloat(ethers3.formatUnits(pathUSD, 6)),
|
|
3463
5144
|
// pathUSD as default USDC
|
|
3464
|
-
usdt: parseFloat(
|
|
5145
|
+
usdt: parseFloat(ethers3.formatUnits(alphaUSD, 6)),
|
|
3465
5146
|
// alphaUSD as default USDT
|
|
3466
|
-
native: parseFloat(
|
|
5147
|
+
native: parseFloat(ethers3.formatEther(nativeBalance)),
|
|
3467
5148
|
tempo: {
|
|
3468
|
-
pathUSD: parseFloat(
|
|
3469
|
-
alphaUSD: parseFloat(
|
|
3470
|
-
betaUSD: parseFloat(
|
|
3471
|
-
thetaUSD: parseFloat(
|
|
5149
|
+
pathUSD: parseFloat(ethers3.formatUnits(pathUSD, 6)),
|
|
5150
|
+
alphaUSD: parseFloat(ethers3.formatUnits(alphaUSD, 6)),
|
|
5151
|
+
betaUSD: parseFloat(ethers3.formatUnits(betaUSD, 6)),
|
|
5152
|
+
thetaUSD: parseFloat(ethers3.formatUnits(thetaUSD, 6))
|
|
3472
5153
|
}
|
|
3473
5154
|
};
|
|
3474
5155
|
} else {
|
|
3475
5156
|
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
3476
5157
|
provider.getBalance(this.wallet.address),
|
|
3477
|
-
new
|
|
3478
|
-
new
|
|
5158
|
+
new ethers3.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
5159
|
+
new ethers3.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
3479
5160
|
]);
|
|
3480
5161
|
results[chainName] = {
|
|
3481
|
-
usdc: parseFloat(
|
|
3482
|
-
usdt: parseFloat(
|
|
3483
|
-
native: parseFloat(
|
|
5162
|
+
usdc: parseFloat(ethers3.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
5163
|
+
usdt: parseFloat(ethers3.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
5164
|
+
native: parseFloat(ethers3.formatEther(nativeBalance))
|
|
3484
5165
|
};
|
|
3485
5166
|
}
|
|
3486
5167
|
} catch (err) {
|
|
@@ -3608,14 +5289,14 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
3608
5289
|
};
|
|
3609
5290
|
|
|
3610
5291
|
// src/wallet/Wallet.ts
|
|
3611
|
-
import { ethers as
|
|
5292
|
+
import { ethers as ethers4 } from "ethers";
|
|
3612
5293
|
|
|
3613
5294
|
// src/wallet/createWallet.ts
|
|
3614
|
-
import { ethers as
|
|
5295
|
+
import { ethers as ethers5 } from "ethers";
|
|
3615
5296
|
import { writeFileSync as writeFileSync3, readFileSync as readFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
3616
|
-
import { join as
|
|
5297
|
+
import { join as join6, dirname as dirname2 } from "path";
|
|
3617
5298
|
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
|
|
3618
|
-
var DEFAULT_STORAGE_DIR =
|
|
5299
|
+
var DEFAULT_STORAGE_DIR = join6(process.env.HOME || "~", ".moltspay");
|
|
3619
5300
|
var DEFAULT_STORAGE_FILE = "wallet.json";
|
|
3620
5301
|
function encryptPrivateKey(privateKey, password) {
|
|
3621
5302
|
const salt = randomBytes(16);
|
|
@@ -3638,7 +5319,7 @@ function decryptPrivateKey(encrypted, password, iv, salt) {
|
|
|
3638
5319
|
return decrypted;
|
|
3639
5320
|
}
|
|
3640
5321
|
function createWallet(options = {}) {
|
|
3641
|
-
const storagePath = options.storagePath ||
|
|
5322
|
+
const storagePath = options.storagePath || join6(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3642
5323
|
if (existsSync5(storagePath) && !options.overwrite) {
|
|
3643
5324
|
try {
|
|
3644
5325
|
const existing = JSON.parse(readFileSync5(storagePath, "utf8"));
|
|
@@ -3656,7 +5337,7 @@ function createWallet(options = {}) {
|
|
|
3656
5337
|
}
|
|
3657
5338
|
}
|
|
3658
5339
|
try {
|
|
3659
|
-
const wallet =
|
|
5340
|
+
const wallet = ethers5.Wallet.createRandom();
|
|
3660
5341
|
const walletData = {
|
|
3661
5342
|
address: wallet.address,
|
|
3662
5343
|
label: options.label,
|
|
@@ -3672,7 +5353,7 @@ function createWallet(options = {}) {
|
|
|
3672
5353
|
} else {
|
|
3673
5354
|
walletData.privateKey = wallet.privateKey;
|
|
3674
5355
|
}
|
|
3675
|
-
const dir =
|
|
5356
|
+
const dir = dirname2(storagePath);
|
|
3676
5357
|
if (!existsSync5(dir)) {
|
|
3677
5358
|
mkdirSync3(dir, { recursive: true });
|
|
3678
5359
|
}
|
|
@@ -3691,7 +5372,7 @@ function createWallet(options = {}) {
|
|
|
3691
5372
|
}
|
|
3692
5373
|
}
|
|
3693
5374
|
function loadWallet(options = {}) {
|
|
3694
|
-
const storagePath = options.storagePath ||
|
|
5375
|
+
const storagePath = options.storagePath || join6(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3695
5376
|
if (!existsSync5(storagePath)) {
|
|
3696
5377
|
return { success: false, error: "Wallet not found. Run createWallet() first." };
|
|
3697
5378
|
}
|
|
@@ -3711,7 +5392,7 @@ function loadWallet(options = {}) {
|
|
|
3711
5392
|
}
|
|
3712
5393
|
}
|
|
3713
5394
|
function getWalletAddress(storagePath) {
|
|
3714
|
-
const path4 = storagePath ||
|
|
5395
|
+
const path4 = storagePath || join6(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3715
5396
|
if (!existsSync5(path4)) {
|
|
3716
5397
|
return null;
|
|
3717
5398
|
}
|
|
@@ -3723,13 +5404,13 @@ function getWalletAddress(storagePath) {
|
|
|
3723
5404
|
}
|
|
3724
5405
|
}
|
|
3725
5406
|
function walletExists(storagePath) {
|
|
3726
|
-
const path4 = storagePath ||
|
|
5407
|
+
const path4 = storagePath || join6(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
|
|
3727
5408
|
return existsSync5(path4);
|
|
3728
5409
|
}
|
|
3729
5410
|
|
|
3730
5411
|
// src/verify/index.ts
|
|
3731
|
-
import { ethers as
|
|
3732
|
-
var TRANSFER_EVENT_TOPIC3 =
|
|
5412
|
+
import { ethers as ethers6 } from "ethers";
|
|
5413
|
+
var TRANSFER_EVENT_TOPIC3 = ethers6.id("Transfer(address,address,uint256)");
|
|
3733
5414
|
async function verifyPayment(params) {
|
|
3734
5415
|
const { txHash, expectedAmount, expectedTo, expectedToken } = params;
|
|
3735
5416
|
let chain;
|
|
@@ -3746,7 +5427,7 @@ async function verifyPayment(params) {
|
|
|
3746
5427
|
return { verified: false, error: `Unsupported chain: ${params.chain}` };
|
|
3747
5428
|
}
|
|
3748
5429
|
try {
|
|
3749
|
-
const provider = new
|
|
5430
|
+
const provider = new ethers6.JsonRpcProvider(chain.rpc);
|
|
3750
5431
|
const receipt = await provider.getTransactionReceipt(txHash);
|
|
3751
5432
|
if (!receipt) {
|
|
3752
5433
|
return { verified: false, error: "Transaction not found or not confirmed" };
|
|
@@ -3818,7 +5499,7 @@ async function getTransactionStatus(txHash, chain = "base") {
|
|
|
3818
5499
|
return { status: "not_found" };
|
|
3819
5500
|
}
|
|
3820
5501
|
try {
|
|
3821
|
-
const provider = new
|
|
5502
|
+
const provider = new ethers6.JsonRpcProvider(chainConfig.rpc);
|
|
3822
5503
|
const receipt = await provider.getTransactionReceipt(txHash);
|
|
3823
5504
|
if (!receipt) {
|
|
3824
5505
|
const tx = await provider.getTransaction(txHash);
|
|
@@ -3855,7 +5536,7 @@ async function waitForTransaction(txHash, chain = "base", confirmations = 1, tim
|
|
|
3855
5536
|
} catch (e) {
|
|
3856
5537
|
return { verified: false, confirmed: false, error: `Unsupported chain: ${chain}` };
|
|
3857
5538
|
}
|
|
3858
|
-
const provider = new
|
|
5539
|
+
const provider = new ethers6.JsonRpcProvider(chainConfig.rpc);
|
|
3859
5540
|
try {
|
|
3860
5541
|
const receipt = await provider.waitForTransaction(txHash, confirmations, timeoutMs);
|
|
3861
5542
|
if (!receipt) {
|
|
@@ -3995,9 +5676,9 @@ var CDPWallet = class {
|
|
|
3995
5676
|
* Get USDC balance
|
|
3996
5677
|
*/
|
|
3997
5678
|
async getBalance() {
|
|
3998
|
-
const { ethers:
|
|
3999
|
-
const provider = new
|
|
4000
|
-
const usdcContract = new
|
|
5679
|
+
const { ethers: ethers7 } = await import("ethers");
|
|
5680
|
+
const provider = new ethers7.JsonRpcProvider(this.chainConfig.rpc);
|
|
5681
|
+
const usdcContract = new ethers7.Contract(
|
|
4001
5682
|
this.chainConfig.usdc,
|
|
4002
5683
|
["function balanceOf(address) view returns (uint256)"],
|
|
4003
5684
|
provider
|
|
@@ -4008,7 +5689,7 @@ var CDPWallet = class {
|
|
|
4008
5689
|
]);
|
|
4009
5690
|
return {
|
|
4010
5691
|
usdc: (Number(usdcBalance) / 1e6).toFixed(2),
|
|
4011
|
-
eth:
|
|
5692
|
+
eth: ethers7.formatEther(ethBalance)
|
|
4012
5693
|
};
|
|
4013
5694
|
}
|
|
4014
5695
|
/**
|
|
@@ -4026,7 +5707,7 @@ var CDPWallet = class {
|
|
|
4026
5707
|
}
|
|
4027
5708
|
try {
|
|
4028
5709
|
const { CdpClient } = await import("@coinbase/cdp-sdk");
|
|
4029
|
-
const { ethers:
|
|
5710
|
+
const { ethers: ethers7 } = await import("ethers");
|
|
4030
5711
|
const cdp = new CdpClient({
|
|
4031
5712
|
apiKeyId: creds.apiKeyId,
|
|
4032
5713
|
apiKeySecret: creds.apiKeySecret,
|
|
@@ -4034,7 +5715,7 @@ var CDPWallet = class {
|
|
|
4034
5715
|
});
|
|
4035
5716
|
const account = await cdp.evm.getAccount({ address: this.address });
|
|
4036
5717
|
const amountWei = BigInt(Math.floor(params.amount * 1e6));
|
|
4037
|
-
const iface = new
|
|
5718
|
+
const iface = new ethers7.Interface([
|
|
4038
5719
|
"function transfer(address to, uint256 amount) returns (bool)"
|
|
4039
5720
|
]);
|
|
4040
5721
|
const callData = iface.encodeFunctionData("transfer", [params.to, amountWei]);
|
|
@@ -4088,8 +5769,10 @@ export {
|
|
|
4088
5769
|
FacilitatorRegistry,
|
|
4089
5770
|
MoltsPayClient,
|
|
4090
5771
|
MoltsPayServer,
|
|
5772
|
+
alipayLog,
|
|
4091
5773
|
createRegistry,
|
|
4092
5774
|
createWallet,
|
|
5775
|
+
getAlipayLogLevel,
|
|
4093
5776
|
getCDPWalletAddress,
|
|
4094
5777
|
getChain,
|
|
4095
5778
|
getChainById,
|
|
@@ -4101,6 +5784,10 @@ export {
|
|
|
4101
5784
|
listChains,
|
|
4102
5785
|
loadCDPWallet,
|
|
4103
5786
|
loadWallet,
|
|
5787
|
+
resetIntentCache,
|
|
5788
|
+
resetWalletCache,
|
|
5789
|
+
setAlipayLogLevel,
|
|
5790
|
+
setAlipayLogSink,
|
|
4104
5791
|
verifyPayment,
|
|
4105
5792
|
waitForTransaction,
|
|
4106
5793
|
walletExists
|