moltspay 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
- package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
- package/dist/chains/index.d.mts +37 -1
- package/dist/chains/index.d.ts +37 -1
- package/dist/chains/index.js +22 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +19 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +1941 -204
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1938 -201
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +46 -0
- package/dist/client/index.d.ts +46 -0
- package/dist/client/index.js +1059 -118
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +1055 -116
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.d.mts +434 -0
- package/dist/client/web/index.mjs +1300 -0
- package/dist/client/web/index.mjs.map +1 -0
- package/dist/facilitators/index.d.mts +185 -6
- package/dist/facilitators/index.d.ts +185 -6
- package/dist/facilitators/index.js +497 -13
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +492 -13
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/index.d.mts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js +1865 -172
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1854 -167
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.js +1062 -123
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/index.mjs +1059 -120
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
- package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
- package/dist/server/index.d.mts +137 -2
- package/dist/server/index.d.ts +137 -2
- package/dist/server/index.js +757 -30
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +757 -30
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +15 -2
- package/schemas/moltspay.services.schema.json +100 -6
- package/scripts/postinstall.js +91 -0
|
@@ -30,6 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/facilitators/index.ts
|
|
31
31
|
var facilitators_exports = {};
|
|
32
32
|
__export(facilitators_exports, {
|
|
33
|
+
ALIPAY_GATEWAY_PROD: () => ALIPAY_GATEWAY_PROD,
|
|
34
|
+
ALIPAY_GATEWAY_SANDBOX: () => ALIPAY_GATEWAY_SANDBOX,
|
|
35
|
+
ALIPAY_NETWORK: () => ALIPAY_NETWORK,
|
|
36
|
+
ALIPAY_SCHEME: () => ALIPAY_SCHEME,
|
|
37
|
+
AlipayFacilitator: () => AlipayFacilitator,
|
|
33
38
|
BNBFacilitator: () => BNBFacilitator,
|
|
34
39
|
BaseFacilitator: () => BaseFacilitator,
|
|
35
40
|
CDPFacilitator: () => CDPFacilitator,
|
|
@@ -269,6 +274,9 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
269
274
|
}
|
|
270
275
|
};
|
|
271
276
|
|
|
277
|
+
// src/facilitators/tempo.ts
|
|
278
|
+
var import_ethers = require("ethers");
|
|
279
|
+
|
|
272
280
|
// src/chains/index.ts
|
|
273
281
|
var CHAINS = {
|
|
274
282
|
// ============ Mainnet ============
|
|
@@ -438,15 +446,38 @@ var CHAINS = {
|
|
|
438
446
|
|
|
439
447
|
// src/facilitators/tempo.ts
|
|
440
448
|
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
449
|
+
var TIP20_PERMIT_ABI = [
|
|
450
|
+
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
|
451
|
+
"function transferFrom(address from, address to, uint256 value) returns (bool)"
|
|
452
|
+
];
|
|
441
453
|
var TempoFacilitator = class extends BaseFacilitator {
|
|
442
454
|
name = "tempo";
|
|
443
455
|
displayName = "Tempo Testnet";
|
|
444
456
|
supportedNetworks = ["eip155:42431"];
|
|
445
457
|
// Tempo Moderato
|
|
446
458
|
rpcUrl;
|
|
459
|
+
settlerWallet = null;
|
|
447
460
|
constructor() {
|
|
448
461
|
super();
|
|
449
462
|
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
463
|
+
const settlerKey = process.env.TEMPO_SETTLER_KEY;
|
|
464
|
+
if (settlerKey) {
|
|
465
|
+
try {
|
|
466
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(this.rpcUrl);
|
|
467
|
+
this.settlerWallet = new import_ethers.ethers.Wallet(settlerKey, provider);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
console.warn("[TempoFacilitator] Invalid TEMPO_SETTLER_KEY, permit settlement disabled:", err);
|
|
470
|
+
this.settlerWallet = null;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Settler EOA address advertised to clients via `X-Payment-Required.extra.tempoSpender`.
|
|
476
|
+
* Web Client uses this as the `spender` field in the signed EIP-2612 Permit.
|
|
477
|
+
* Returns null if no TEMPO_SETTLER_KEY is configured — permit settlement unavailable.
|
|
478
|
+
*/
|
|
479
|
+
getSpenderAddress() {
|
|
480
|
+
return this.settlerWallet?.address ?? null;
|
|
450
481
|
}
|
|
451
482
|
async healthCheck() {
|
|
452
483
|
const start = Date.now();
|
|
@@ -472,6 +503,44 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
472
503
|
}
|
|
473
504
|
}
|
|
474
505
|
async verify(paymentPayload, requirements) {
|
|
506
|
+
const inner = paymentPayload.payload;
|
|
507
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
508
|
+
return this.verifyPermit(inner, requirements);
|
|
509
|
+
}
|
|
510
|
+
return this.verifyTxHash(paymentPayload, requirements);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Structural validation of an EIP-2612 permit payload. Does NOT submit
|
|
514
|
+
* anything on-chain — actual submission happens in settlePermit().
|
|
515
|
+
*/
|
|
516
|
+
async verifyPermit(payload, requirements) {
|
|
517
|
+
if (!this.settlerWallet) {
|
|
518
|
+
return { valid: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
519
|
+
}
|
|
520
|
+
const p = payload.permit;
|
|
521
|
+
if (!p || !p.owner || !p.spender || !p.value || !p.deadline) {
|
|
522
|
+
return { valid: false, error: "Invalid permit payload: missing fields" };
|
|
523
|
+
}
|
|
524
|
+
if (p.spender.toLowerCase() !== this.settlerWallet.address.toLowerCase()) {
|
|
525
|
+
return {
|
|
526
|
+
valid: false,
|
|
527
|
+
error: `Permit spender ${p.spender} does not match configured settler ${this.settlerWallet.address}`
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
const deadline = BigInt(p.deadline);
|
|
531
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
532
|
+
if (deadline <= now) {
|
|
533
|
+
return { valid: false, error: "Permit deadline has expired" };
|
|
534
|
+
}
|
|
535
|
+
if (BigInt(p.value) < BigInt(requirements.amount || "0")) {
|
|
536
|
+
return {
|
|
537
|
+
valid: false,
|
|
538
|
+
error: `Permit value ${p.value} is less than required ${requirements.amount}`
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return { valid: true, details: { scheme: "permit", owner: p.owner } };
|
|
542
|
+
}
|
|
543
|
+
async verifyTxHash(paymentPayload, requirements) {
|
|
475
544
|
try {
|
|
476
545
|
const tempoPayload = paymentPayload.payload;
|
|
477
546
|
if (!tempoPayload?.txHash) {
|
|
@@ -529,7 +598,11 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
529
598
|
}
|
|
530
599
|
}
|
|
531
600
|
async settle(paymentPayload, requirements) {
|
|
532
|
-
const
|
|
601
|
+
const inner = paymentPayload.payload;
|
|
602
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
603
|
+
return this.settlePermit(inner, requirements);
|
|
604
|
+
}
|
|
605
|
+
const verifyResult = await this.verifyTxHash(paymentPayload, requirements);
|
|
533
606
|
if (!verifyResult.valid) {
|
|
534
607
|
return { success: false, error: verifyResult.error };
|
|
535
608
|
}
|
|
@@ -540,6 +613,52 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
540
613
|
status: "settled"
|
|
541
614
|
};
|
|
542
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* EIP-2612 permit settlement path. Submits two transactions on Tempo:
|
|
618
|
+
* 1. pathUSD.permit(owner, spender=settler, value, deadline, v, r, s)
|
|
619
|
+
* 2. pathUSD.transferFrom(owner, payTo, value)
|
|
620
|
+
*
|
|
621
|
+
* The settler EOA pays Tempo gas (via the TIP-20 `feeToken` mechanism — no
|
|
622
|
+
* native tTEMPO required; any held TIP-20 token balance covers fees).
|
|
623
|
+
*/
|
|
624
|
+
async settlePermit(payload, requirements) {
|
|
625
|
+
if (!this.settlerWallet) {
|
|
626
|
+
return { success: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
627
|
+
}
|
|
628
|
+
if (!requirements.asset || !requirements.payTo) {
|
|
629
|
+
return { success: false, error: "Missing asset or payTo in requirements" };
|
|
630
|
+
}
|
|
631
|
+
const verifyResult = await this.verifyPermit(payload, requirements);
|
|
632
|
+
if (!verifyResult.valid) {
|
|
633
|
+
return { success: false, error: verifyResult.error };
|
|
634
|
+
}
|
|
635
|
+
const token = new import_ethers.ethers.Contract(requirements.asset, TIP20_PERMIT_ABI, this.settlerWallet);
|
|
636
|
+
const p = payload.permit;
|
|
637
|
+
try {
|
|
638
|
+
const permitTx = await token.permit(
|
|
639
|
+
p.owner,
|
|
640
|
+
p.spender,
|
|
641
|
+
p.value,
|
|
642
|
+
p.deadline,
|
|
643
|
+
p.v,
|
|
644
|
+
p.r,
|
|
645
|
+
p.s
|
|
646
|
+
);
|
|
647
|
+
await permitTx.wait();
|
|
648
|
+
const transferTx = await token.transferFrom(p.owner, requirements.payTo, p.value);
|
|
649
|
+
await transferTx.wait();
|
|
650
|
+
return {
|
|
651
|
+
success: true,
|
|
652
|
+
transaction: transferTx.hash,
|
|
653
|
+
status: "settled"
|
|
654
|
+
};
|
|
655
|
+
} catch (err) {
|
|
656
|
+
return {
|
|
657
|
+
success: false,
|
|
658
|
+
error: `Tempo permit settlement failed: ${err.message}`
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
543
662
|
async getTransactionReceipt(txHash) {
|
|
544
663
|
const response = await fetch(this.rpcUrl, {
|
|
545
664
|
method: "POST",
|
|
@@ -782,12 +901,12 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
782
901
|
return this.spenderAddress;
|
|
783
902
|
}
|
|
784
903
|
async getServerAddress() {
|
|
785
|
-
const { ethers } = await import("ethers");
|
|
786
|
-
const wallet = new
|
|
904
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
905
|
+
const wallet = new ethers2.Wallet(this.serverPrivateKey);
|
|
787
906
|
return wallet.address;
|
|
788
907
|
}
|
|
789
908
|
async recoverIntentSigner(intent, chainId) {
|
|
790
|
-
const { ethers } = await import("ethers");
|
|
909
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
791
910
|
const domain = {
|
|
792
911
|
...EIP712_DOMAIN,
|
|
793
912
|
chainId
|
|
@@ -801,7 +920,7 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
801
920
|
nonce: intent.nonce,
|
|
802
921
|
deadline: intent.deadline
|
|
803
922
|
};
|
|
804
|
-
const recoveredAddress =
|
|
923
|
+
const recoveredAddress = ethers2.verifyTypedData(
|
|
805
924
|
domain,
|
|
806
925
|
INTENT_TYPES,
|
|
807
926
|
message,
|
|
@@ -845,10 +964,10 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
845
964
|
return result.result || "0x0";
|
|
846
965
|
}
|
|
847
966
|
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
848
|
-
const { ethers } = await import("ethers");
|
|
849
|
-
const provider = new
|
|
850
|
-
const wallet = new
|
|
851
|
-
const tokenContract = new
|
|
967
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
968
|
+
const provider = new ethers2.JsonRpcProvider(rpcUrl);
|
|
969
|
+
const wallet = new ethers2.Wallet(this.serverPrivateKey, provider);
|
|
970
|
+
const tokenContract = new ethers2.Contract(token, [
|
|
852
971
|
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
853
972
|
], wallet);
|
|
854
973
|
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
@@ -1093,16 +1212,16 @@ var SolanaFacilitator = class extends BaseFacilitator {
|
|
|
1093
1212
|
return this.supportedNetworks.includes(network);
|
|
1094
1213
|
}
|
|
1095
1214
|
};
|
|
1096
|
-
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
1215
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey, connection) {
|
|
1097
1216
|
const chainConfig = SOLANA_CHAINS[chain];
|
|
1098
|
-
const
|
|
1217
|
+
const conn = connection ?? new import_web32.Connection(chainConfig.rpc, "confirmed");
|
|
1099
1218
|
const mint = new import_web32.PublicKey(chainConfig.tokens.USDC.mint);
|
|
1100
1219
|
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
1101
1220
|
const senderATA = await (0, import_spl_token.getAssociatedTokenAddress)(mint, senderPubkey);
|
|
1102
1221
|
const recipientATA = await (0, import_spl_token.getAssociatedTokenAddress)(mint, recipientPubkey);
|
|
1103
1222
|
const transaction = new import_web32.Transaction();
|
|
1104
1223
|
try {
|
|
1105
|
-
await (0, import_spl_token.getAccount)(
|
|
1224
|
+
await (0, import_spl_token.getAccount)(conn, recipientATA);
|
|
1106
1225
|
} catch {
|
|
1107
1226
|
transaction.add(
|
|
1108
1227
|
(0, import_spl_token.createAssociatedTokenAccountInstruction)(
|
|
@@ -1133,12 +1252,371 @@ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amo
|
|
|
1133
1252
|
// decimals
|
|
1134
1253
|
)
|
|
1135
1254
|
);
|
|
1136
|
-
const { blockhash, lastValidBlockHeight } = await
|
|
1255
|
+
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash();
|
|
1137
1256
|
transaction.recentBlockhash = blockhash;
|
|
1138
1257
|
transaction.feePayer = actualFeePayer;
|
|
1139
1258
|
return transaction;
|
|
1140
1259
|
}
|
|
1141
1260
|
|
|
1261
|
+
// src/facilitators/alipay.ts
|
|
1262
|
+
var import_node_crypto2 = __toESM(require("crypto"));
|
|
1263
|
+
|
|
1264
|
+
// src/facilitators/alipay/rsa2.ts
|
|
1265
|
+
var import_node_crypto = __toESM(require("crypto"));
|
|
1266
|
+
function rsa2Sign(data, privateKeyPem) {
|
|
1267
|
+
const signer = import_node_crypto.default.createSign("RSA-SHA256");
|
|
1268
|
+
signer.update(data, "utf-8");
|
|
1269
|
+
signer.end();
|
|
1270
|
+
return signer.sign(privateKeyPem, "base64");
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/facilitators/alipay/encoding.ts
|
|
1274
|
+
function base64url(input) {
|
|
1275
|
+
return Buffer.from(input, "utf-8").toString("base64url");
|
|
1276
|
+
}
|
|
1277
|
+
function decodeBase64UrlWithPadFix(input) {
|
|
1278
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1279
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
1280
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/facilitators/alipay/openapi.ts
|
|
1284
|
+
function formatAlipayTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
1285
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1286
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1287
|
+
}
|
|
1288
|
+
function buildSigningString(params) {
|
|
1289
|
+
return Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
1290
|
+
}
|
|
1291
|
+
function responseWrapperKey(method) {
|
|
1292
|
+
return `${method.replace(/\./g, "_")}_response`;
|
|
1293
|
+
}
|
|
1294
|
+
async function alipayOpenApiCall(method, bizContent, config) {
|
|
1295
|
+
const publicParams = {
|
|
1296
|
+
app_id: config.app_id,
|
|
1297
|
+
method,
|
|
1298
|
+
format: "JSON",
|
|
1299
|
+
charset: "utf-8",
|
|
1300
|
+
sign_type: config.sign_type ?? "RSA2",
|
|
1301
|
+
timestamp: formatAlipayTimestamp(),
|
|
1302
|
+
version: "1.0",
|
|
1303
|
+
biz_content: JSON.stringify(bizContent)
|
|
1304
|
+
};
|
|
1305
|
+
const signingString = buildSigningString(publicParams);
|
|
1306
|
+
const sign = rsa2Sign(signingString, config.private_key_pem);
|
|
1307
|
+
const body = new URLSearchParams({ ...publicParams, sign }).toString();
|
|
1308
|
+
const response = await fetch(config.gateway_url, {
|
|
1309
|
+
method: "POST",
|
|
1310
|
+
headers: {
|
|
1311
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
|
|
1312
|
+
},
|
|
1313
|
+
body
|
|
1314
|
+
});
|
|
1315
|
+
if (!response.ok) {
|
|
1316
|
+
const text = await response.text().catch(() => "<unreadable>");
|
|
1317
|
+
throw new Error(
|
|
1318
|
+
`Alipay Open API HTTP ${response.status} for ${method}: ${text.slice(0, 500)}`
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
const json = await response.json();
|
|
1322
|
+
const wrapperKey = responseWrapperKey(method);
|
|
1323
|
+
const business = json[wrapperKey];
|
|
1324
|
+
if (business === void 0 || business === null || typeof business !== "object") {
|
|
1325
|
+
throw new Error(
|
|
1326
|
+
`Alipay Open API response missing "${wrapperKey}" wrapper for ${method}: ${JSON.stringify(json).slice(0, 500)}`
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
return business;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/facilitators/alipay.ts
|
|
1333
|
+
var ALIPAY_NETWORK = "alipay";
|
|
1334
|
+
var ALIPAY_SCHEME = "alipay-aipay";
|
|
1335
|
+
var ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
|
|
1336
|
+
var ALIPAY_GATEWAY_SANDBOX = "https://openapi.alipaydev.com/gateway.do";
|
|
1337
|
+
var ALIPAY_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
|
|
1338
|
+
var ALIPAY_PAY_BEFORE_MS = 30 * 60 * 1e3;
|
|
1339
|
+
var ALIPAY_SIGNING_FIELDS = [
|
|
1340
|
+
"amount",
|
|
1341
|
+
"currency",
|
|
1342
|
+
"goods_name",
|
|
1343
|
+
"out_trade_no",
|
|
1344
|
+
"pay_before",
|
|
1345
|
+
"resource_id",
|
|
1346
|
+
"seller_id",
|
|
1347
|
+
"service_id"
|
|
1348
|
+
];
|
|
1349
|
+
var AlipayFacilitator = class extends BaseFacilitator {
|
|
1350
|
+
name = "alipay";
|
|
1351
|
+
displayName = "Alipay AI \u6536";
|
|
1352
|
+
supportedNetworks = [ALIPAY_NETWORK];
|
|
1353
|
+
config;
|
|
1354
|
+
constructor(config) {
|
|
1355
|
+
super();
|
|
1356
|
+
this.config = {
|
|
1357
|
+
gateway_url: ALIPAY_GATEWAY_PROD,
|
|
1358
|
+
sign_type: "RSA2",
|
|
1359
|
+
...config
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Build the 402 challenge for a service: signs the 8-field payload with
|
|
1364
|
+
* RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
|
|
1365
|
+
* `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
|
|
1366
|
+
*/
|
|
1367
|
+
async createPaymentRequirements(opts) {
|
|
1368
|
+
if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
|
|
1369
|
+
throw new Error(
|
|
1370
|
+
`AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
const now = /* @__PURE__ */ new Date();
|
|
1374
|
+
const outTradeNo = opts.outTradeNo ?? generateOutTradeNo();
|
|
1375
|
+
const payBefore = formatPayBefore(now);
|
|
1376
|
+
const signedFields = {
|
|
1377
|
+
amount: opts.priceCny,
|
|
1378
|
+
currency: "CNY",
|
|
1379
|
+
goods_name: opts.goodsName,
|
|
1380
|
+
out_trade_no: outTradeNo,
|
|
1381
|
+
pay_before: payBefore,
|
|
1382
|
+
resource_id: opts.resourceId,
|
|
1383
|
+
seller_id: this.config.seller_id,
|
|
1384
|
+
service_id: opts.serviceId
|
|
1385
|
+
};
|
|
1386
|
+
const signingString = ALIPAY_SIGNING_FIELDS.map((k) => `${k}=${signedFields[k]}`).join("&");
|
|
1387
|
+
const seller_signature = rsa2Sign(signingString, this.config.private_key_pem);
|
|
1388
|
+
const challenge = {
|
|
1389
|
+
protocol: {
|
|
1390
|
+
out_trade_no: outTradeNo,
|
|
1391
|
+
amount: signedFields.amount,
|
|
1392
|
+
currency: signedFields.currency,
|
|
1393
|
+
resource_id: signedFields.resource_id,
|
|
1394
|
+
pay_before: payBefore,
|
|
1395
|
+
seller_signature,
|
|
1396
|
+
seller_sign_type: this.config.sign_type ?? "RSA2",
|
|
1397
|
+
seller_unique_id: this.config.seller_id
|
|
1398
|
+
},
|
|
1399
|
+
method: {
|
|
1400
|
+
seller_name: this.config.seller_name,
|
|
1401
|
+
seller_id: this.config.seller_id,
|
|
1402
|
+
seller_app_id: this.config.app_id,
|
|
1403
|
+
goods_name: signedFields.goods_name,
|
|
1404
|
+
seller_unique_id_key: "seller_id",
|
|
1405
|
+
service_id: signedFields.service_id
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
const paymentNeededHeader = base64url(JSON.stringify(challenge));
|
|
1409
|
+
const x402Accepts = {
|
|
1410
|
+
scheme: ALIPAY_SCHEME,
|
|
1411
|
+
network: ALIPAY_NETWORK,
|
|
1412
|
+
asset: "CNY",
|
|
1413
|
+
amount: opts.priceCny,
|
|
1414
|
+
payTo: this.config.seller_id,
|
|
1415
|
+
maxTimeoutSeconds: ALIPAY_PAY_BEFORE_MS / 1e3,
|
|
1416
|
+
extra: {
|
|
1417
|
+
payment_needed_header: paymentNeededHeader,
|
|
1418
|
+
out_trade_no: outTradeNo,
|
|
1419
|
+
pay_before: payBefore,
|
|
1420
|
+
service_id: signedFields.service_id
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
return { x402Accepts, paymentNeededHeader };
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Verify a `Payment-Proof` by calling
|
|
1427
|
+
* `alipay.aipay.agent.payment.verify` against the Alipay Open API.
|
|
1428
|
+
*
|
|
1429
|
+
* The proof is conveyed via `paymentPayload.payload`, which may be:
|
|
1430
|
+
* - a raw Base64URL string (the `Payment-Proof` header value), or
|
|
1431
|
+
* - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
|
|
1432
|
+
*
|
|
1433
|
+
* All failure modes (malformed payload, network errors, Alipay
|
|
1434
|
+
* `code != 10000`) return `{ valid: false, error }`. No exception
|
|
1435
|
+
* escapes, regardless of client-supplied input.
|
|
1436
|
+
*/
|
|
1437
|
+
async verify(paymentPayload, _requirements) {
|
|
1438
|
+
try {
|
|
1439
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1440
|
+
const decoded = decodeProof(proofHeader);
|
|
1441
|
+
const response = await alipayOpenApiCall(
|
|
1442
|
+
"alipay.aipay.agent.payment.verify",
|
|
1443
|
+
{
|
|
1444
|
+
payment_proof: decoded.protocol.payment_proof,
|
|
1445
|
+
trade_no: decoded.protocol.trade_no,
|
|
1446
|
+
client_session: decoded.method.client_session
|
|
1447
|
+
},
|
|
1448
|
+
this.getOpenApiConfig()
|
|
1449
|
+
);
|
|
1450
|
+
if (response.code !== "10000") {
|
|
1451
|
+
return {
|
|
1452
|
+
valid: false,
|
|
1453
|
+
error: `alipay verify ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1454
|
+
details: {
|
|
1455
|
+
code: response.code,
|
|
1456
|
+
sub_code: response.sub_code,
|
|
1457
|
+
sub_msg: response.sub_msg
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
valid: true,
|
|
1463
|
+
details: {
|
|
1464
|
+
trade_no: response.trade_no ?? decoded.protocol.trade_no,
|
|
1465
|
+
amount: response.amount,
|
|
1466
|
+
out_trade_no: response.out_trade_no,
|
|
1467
|
+
resource_id: response.resource_id,
|
|
1468
|
+
active: response.active
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
} catch (e) {
|
|
1472
|
+
return {
|
|
1473
|
+
valid: false,
|
|
1474
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
|
|
1480
|
+
* service resource has been returned to the buyer.
|
|
1481
|
+
*
|
|
1482
|
+
* Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
|
|
1483
|
+
* "履约确认失败"), this is **fire-and-forget**: the caller (registry /
|
|
1484
|
+
* server) is expected to log fulfillment failures but NOT roll back
|
|
1485
|
+
* the already-delivered resource.
|
|
1486
|
+
*
|
|
1487
|
+
* Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
|
|
1488
|
+
* does the same; the redundant Base64URL decode is negligible).
|
|
1489
|
+
*/
|
|
1490
|
+
async settle(paymentPayload, _requirements) {
|
|
1491
|
+
try {
|
|
1492
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1493
|
+
const decoded = decodeProof(proofHeader);
|
|
1494
|
+
const tradeNo = decoded.protocol.trade_no;
|
|
1495
|
+
const response = await alipayOpenApiCall(
|
|
1496
|
+
"alipay.aipay.agent.fulfillment.confirm",
|
|
1497
|
+
{ trade_no: tradeNo },
|
|
1498
|
+
this.getOpenApiConfig()
|
|
1499
|
+
);
|
|
1500
|
+
if (response.code !== "10000") {
|
|
1501
|
+
return {
|
|
1502
|
+
success: false,
|
|
1503
|
+
transaction: tradeNo,
|
|
1504
|
+
error: `alipay fulfillment ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1505
|
+
status: "fulfillment_failed"
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
return {
|
|
1509
|
+
success: true,
|
|
1510
|
+
transaction: tradeNo,
|
|
1511
|
+
status: "fulfilled"
|
|
1512
|
+
};
|
|
1513
|
+
} catch (e) {
|
|
1514
|
+
return {
|
|
1515
|
+
success: false,
|
|
1516
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Validate that the configured RSA keys parse and that the Open API
|
|
1522
|
+
* gateway is reachable. Does NOT make a real business API call (would
|
|
1523
|
+
* burn quota and could appear as a legitimate verify attempt in logs).
|
|
1524
|
+
*/
|
|
1525
|
+
async healthCheck() {
|
|
1526
|
+
const start = Date.now();
|
|
1527
|
+
try {
|
|
1528
|
+
import_node_crypto2.default.createPrivateKey(this.config.private_key_pem);
|
|
1529
|
+
} catch (e) {
|
|
1530
|
+
return {
|
|
1531
|
+
healthy: false,
|
|
1532
|
+
error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
try {
|
|
1536
|
+
import_node_crypto2.default.createPublicKey(this.config.alipay_public_key_pem);
|
|
1537
|
+
} catch (e) {
|
|
1538
|
+
return {
|
|
1539
|
+
healthy: false,
|
|
1540
|
+
error: `alipay_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
const gatewayUrl = this.config.gateway_url ?? ALIPAY_GATEWAY_PROD;
|
|
1544
|
+
const controller = new AbortController();
|
|
1545
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
1546
|
+
const response = await fetch(gatewayUrl, {
|
|
1547
|
+
method: "HEAD",
|
|
1548
|
+
signal: controller.signal
|
|
1549
|
+
}).catch(() => null);
|
|
1550
|
+
clearTimeout(timeout);
|
|
1551
|
+
const latencyMs = Date.now() - start;
|
|
1552
|
+
if (!response) {
|
|
1553
|
+
return { healthy: false, error: `gateway unreachable: ${gatewayUrl}`, latencyMs };
|
|
1554
|
+
}
|
|
1555
|
+
return { healthy: true, latencyMs };
|
|
1556
|
+
}
|
|
1557
|
+
/** Bundle the facilitator config into the shape openapi.ts wants. */
|
|
1558
|
+
getOpenApiConfig() {
|
|
1559
|
+
return {
|
|
1560
|
+
gateway_url: this.config.gateway_url ?? ALIPAY_GATEWAY_PROD,
|
|
1561
|
+
app_id: this.config.app_id,
|
|
1562
|
+
private_key_pem: this.config.private_key_pem,
|
|
1563
|
+
alipay_public_key_pem: this.config.alipay_public_key_pem,
|
|
1564
|
+
sign_type: this.config.sign_type
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
function generateOutTradeNo() {
|
|
1569
|
+
return "VID" + import_node_crypto2.default.randomBytes(22).toString("base64url").slice(0, 29);
|
|
1570
|
+
}
|
|
1571
|
+
function formatPayBefore(now) {
|
|
1572
|
+
const expiry = new Date(now.getTime() + ALIPAY_PAY_BEFORE_MS);
|
|
1573
|
+
return expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1574
|
+
}
|
|
1575
|
+
function extractProofHeader(payload) {
|
|
1576
|
+
if (typeof payload === "string") {
|
|
1577
|
+
if (payload.length === 0) {
|
|
1578
|
+
throw new Error("alipay Payment-Proof is empty");
|
|
1579
|
+
}
|
|
1580
|
+
return payload;
|
|
1581
|
+
}
|
|
1582
|
+
if (payload !== null && typeof payload === "object") {
|
|
1583
|
+
const obj = payload;
|
|
1584
|
+
const candidate = obj.paymentProof ?? obj.proofHeader;
|
|
1585
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
1586
|
+
return candidate;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
throw new Error(
|
|
1590
|
+
"alipay payment payload must be a Base64URL string or {paymentProof: string} / {proofHeader: string}"
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
function decodeProof(proofHeader) {
|
|
1594
|
+
let parsed;
|
|
1595
|
+
try {
|
|
1596
|
+
parsed = JSON.parse(decodeBase64UrlWithPadFix(proofHeader));
|
|
1597
|
+
} catch (e) {
|
|
1598
|
+
throw new Error(
|
|
1599
|
+
`failed to decode Payment-Proof: ${e instanceof Error ? e.message : String(e)}`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1603
|
+
throw new Error("decoded Payment-Proof is not an object");
|
|
1604
|
+
}
|
|
1605
|
+
const obj = parsed;
|
|
1606
|
+
const protocol = obj.protocol;
|
|
1607
|
+
const method = obj.method;
|
|
1608
|
+
if (!protocol || typeof protocol.payment_proof !== "string") {
|
|
1609
|
+
throw new Error("decoded Payment-Proof missing protocol.payment_proof");
|
|
1610
|
+
}
|
|
1611
|
+
if (typeof protocol.trade_no !== "string") {
|
|
1612
|
+
throw new Error("decoded Payment-Proof missing protocol.trade_no");
|
|
1613
|
+
}
|
|
1614
|
+
if (!method || typeof method.client_session !== "string") {
|
|
1615
|
+
throw new Error("decoded Payment-Proof missing method.client_session");
|
|
1616
|
+
}
|
|
1617
|
+
return parsed;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1142
1620
|
// src/facilitators/registry.ts
|
|
1143
1621
|
var import_web33 = require("@solana/web3.js");
|
|
1144
1622
|
var import_bs58 = __toESM(require("bs58"));
|
|
@@ -1163,6 +1641,7 @@ var FacilitatorRegistry = class {
|
|
|
1163
1641
|
}
|
|
1164
1642
|
return new SolanaFacilitator({ feePayerKeypair });
|
|
1165
1643
|
});
|
|
1644
|
+
this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
|
|
1166
1645
|
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
1167
1646
|
}
|
|
1168
1647
|
/**
|
|
@@ -1390,6 +1869,11 @@ function createRegistry(selection) {
|
|
|
1390
1869
|
}
|
|
1391
1870
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1392
1871
|
0 && (module.exports = {
|
|
1872
|
+
ALIPAY_GATEWAY_PROD,
|
|
1873
|
+
ALIPAY_GATEWAY_SANDBOX,
|
|
1874
|
+
ALIPAY_NETWORK,
|
|
1875
|
+
ALIPAY_SCHEME,
|
|
1876
|
+
AlipayFacilitator,
|
|
1393
1877
|
BNBFacilitator,
|
|
1394
1878
|
BaseFacilitator,
|
|
1395
1879
|
CDPFacilitator,
|