moltspay 1.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +143 -0
  2. package/dist/cdp/index.js.map +1 -1
  3. package/dist/cdp/index.mjs.map +1 -1
  4. package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
  5. package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
  6. package/dist/chains/index.d.mts +37 -1
  7. package/dist/chains/index.d.ts +37 -1
  8. package/dist/chains/index.js +22 -0
  9. package/dist/chains/index.js.map +1 -1
  10. package/dist/chains/index.mjs +19 -0
  11. package/dist/chains/index.mjs.map +1 -1
  12. package/dist/cli/index.js +1941 -204
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/index.mjs +1938 -201
  15. package/dist/cli/index.mjs.map +1 -1
  16. package/dist/client/index.d.mts +46 -0
  17. package/dist/client/index.d.ts +46 -0
  18. package/dist/client/index.js +1059 -118
  19. package/dist/client/index.js.map +1 -1
  20. package/dist/client/index.mjs +1055 -116
  21. package/dist/client/index.mjs.map +1 -1
  22. package/dist/client/web/index.d.mts +434 -0
  23. package/dist/client/web/index.mjs +1300 -0
  24. package/dist/client/web/index.mjs.map +1 -0
  25. package/dist/facilitators/index.d.mts +185 -6
  26. package/dist/facilitators/index.d.ts +185 -6
  27. package/dist/facilitators/index.js +497 -13
  28. package/dist/facilitators/index.js.map +1 -1
  29. package/dist/facilitators/index.mjs +492 -13
  30. package/dist/facilitators/index.mjs.map +1 -1
  31. package/dist/index.d.mts +77 -2
  32. package/dist/index.d.ts +77 -2
  33. package/dist/index.js +1865 -172
  34. package/dist/index.js.map +1 -1
  35. package/dist/index.mjs +1854 -167
  36. package/dist/index.mjs.map +1 -1
  37. package/dist/mcp/index.js +1062 -123
  38. package/dist/mcp/index.js.map +1 -1
  39. package/dist/mcp/index.mjs +1059 -120
  40. package/dist/mcp/index.mjs.map +1 -1
  41. package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
  42. package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
  43. package/dist/server/index.d.mts +137 -2
  44. package/dist/server/index.d.ts +137 -2
  45. package/dist/server/index.js +757 -30
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/server/index.mjs +757 -30
  48. package/dist/server/index.mjs.map +1 -1
  49. package/dist/verify/index.js.map +1 -1
  50. package/dist/verify/index.mjs.map +1 -1
  51. package/dist/wallet/index.js.map +1 -1
  52. package/dist/wallet/index.mjs.map +1 -1
  53. package/package.json +15 -2
  54. package/schemas/moltspay.services.schema.json +100 -6
  55. package/scripts/postinstall.js +91 -0
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 verifyResult = await this.verify(paymentPayload, requirements);
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: ethers5 } = await import("ethers");
779
- const wallet = new ethers5.Wallet(this.serverPrivateKey);
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: ethers5 } = await import("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 = ethers5.verifyTypedData(
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: ethers5 } = await import("ethers");
842
- const provider = new ethers5.JsonRpcProvider(rpcUrl);
843
- const wallet = new ethers5.Wallet(this.serverPrivateKey, provider);
844
- const tokenContract = new ethers5.Contract(token, [
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 connection = new Connection2(chainConfig.rpc, "confirmed");
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(connection, recipientATA);
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 connection.getLatestBlockhash();
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: "pathUSD", version: "1" },
1458
- USDT: { name: "alphaUSD", version: "1" }
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
- res.setHeader("Access-Control-Allow-Origin", "*");
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
- return await this.handleExecute(body, paymentHeader, res);
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
- return await this.handleMPPRequest(skill, body, authHeader, x402Header, res);
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
- res.writeHead(402, {
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: "exact",
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((resolve, reject) => {
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
- resolve(body ? JSON.parse(body) : {});
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 homedir2 } from "os";
2618
- import { join as join4 } from "path";
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 || join4(homedir2(), ".moltspay");
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 Wallet(this.walletData.privateKey);
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 ethers.JsonRpcProvider(chain.rpc);
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 = ethers.parseEther("0.0005");
4756
+ const minGasBalance = ethers3.parseEther("0.0005");
3049
4757
  if (nativeBalance < minGasBalance) {
3050
- const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
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 intent = {
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
- token: tokenConfig.address,
4798
+ tokenAddress: tokenConfig.address,
3089
4799
  service,
3090
- nonce: Date.now(),
3091
- // Use timestamp as nonce for simplicity
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.wallet.signTypedData(domain, types, intent);
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
- if (feePayerPubkey) {
3187
- transaction.partialSign(solanaWallet);
3188
- } else {
3189
- transaction.sign(solanaWallet);
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 ethers.Contract(
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 authorization = {
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
- validAfter: validAfter.toString(),
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
- verifyingContract: tokenConfig.address
3271
- };
3272
- const types = {
3273
- TransferWithAuthorization: [
3274
- { name: "from", type: "address" },
3275
- { name: "to", type: "address" },
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 = join4(this.configDir, "config.json");
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 = join4(this.configDir, "config.json");
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 = join4(this.configDir, "spending.json");
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 = join4(this.configDir, "spending.json");
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 = join4(this.configDir, "wallet.json");
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 = Wallet.createRandom();
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 = join4(configDir, "wallet.json");
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 = join4(configDir, "config.json");
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 ethers.JsonRpcProvider(chain.rpc);
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 ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
3424
- new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
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(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
3428
- usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
3429
- native: parseFloat(ethers.formatEther(nativeBalance))
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 ethers.JsonRpcProvider(chain.rpc);
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 ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
3457
- new ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
3458
- new ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
3459
- new ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
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(ethers.formatUnits(pathUSD, 6)),
5143
+ usdc: parseFloat(ethers3.formatUnits(pathUSD, 6)),
3463
5144
  // pathUSD as default USDC
3464
- usdt: parseFloat(ethers.formatUnits(alphaUSD, 6)),
5145
+ usdt: parseFloat(ethers3.formatUnits(alphaUSD, 6)),
3465
5146
  // alphaUSD as default USDT
3466
- native: parseFloat(ethers.formatEther(nativeBalance)),
5147
+ native: parseFloat(ethers3.formatEther(nativeBalance)),
3467
5148
  tempo: {
3468
- pathUSD: parseFloat(ethers.formatUnits(pathUSD, 6)),
3469
- alphaUSD: parseFloat(ethers.formatUnits(alphaUSD, 6)),
3470
- betaUSD: parseFloat(ethers.formatUnits(betaUSD, 6)),
3471
- thetaUSD: parseFloat(ethers.formatUnits(thetaUSD, 6))
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 ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
3478
- new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
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(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
3482
- usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
3483
- native: parseFloat(ethers.formatEther(nativeBalance))
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 ethers2 } from "ethers";
5292
+ import { ethers as ethers4 } from "ethers";
3612
5293
 
3613
5294
  // src/wallet/createWallet.ts
3614
- import { ethers as ethers3 } from "ethers";
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 join5, dirname } from "path";
5297
+ import { join as join6, dirname as dirname2 } from "path";
3617
5298
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
3618
- var DEFAULT_STORAGE_DIR = join5(process.env.HOME || "~", ".moltspay");
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 || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
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 = ethers3.Wallet.createRandom();
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 = dirname(storagePath);
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 || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
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 || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
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 || join5(DEFAULT_STORAGE_DIR, DEFAULT_STORAGE_FILE);
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 ethers4 } from "ethers";
3732
- var TRANSFER_EVENT_TOPIC3 = ethers4.id("Transfer(address,address,uint256)");
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 ethers4.JsonRpcProvider(chain.rpc);
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 ethers4.JsonRpcProvider(chainConfig.rpc);
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 ethers4.JsonRpcProvider(chainConfig.rpc);
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: ethers5 } = await import("ethers");
3999
- const provider = new ethers5.JsonRpcProvider(this.chainConfig.rpc);
4000
- const usdcContract = new ethers5.Contract(
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: ethers5.formatEther(ethBalance)
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: ethers5 } = await import("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 ethers5.Interface([
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