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
|
@@ -224,6 +224,9 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
224
224
|
}
|
|
225
225
|
};
|
|
226
226
|
|
|
227
|
+
// src/facilitators/tempo.ts
|
|
228
|
+
import { ethers } from "ethers";
|
|
229
|
+
|
|
227
230
|
// src/chains/index.ts
|
|
228
231
|
var CHAINS = {
|
|
229
232
|
// ============ Mainnet ============
|
|
@@ -393,15 +396,38 @@ var CHAINS = {
|
|
|
393
396
|
|
|
394
397
|
// src/facilitators/tempo.ts
|
|
395
398
|
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
399
|
+
var TIP20_PERMIT_ABI = [
|
|
400
|
+
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
|
401
|
+
"function transferFrom(address from, address to, uint256 value) returns (bool)"
|
|
402
|
+
];
|
|
396
403
|
var TempoFacilitator = class extends BaseFacilitator {
|
|
397
404
|
name = "tempo";
|
|
398
405
|
displayName = "Tempo Testnet";
|
|
399
406
|
supportedNetworks = ["eip155:42431"];
|
|
400
407
|
// Tempo Moderato
|
|
401
408
|
rpcUrl;
|
|
409
|
+
settlerWallet = null;
|
|
402
410
|
constructor() {
|
|
403
411
|
super();
|
|
404
412
|
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
413
|
+
const settlerKey = process.env.TEMPO_SETTLER_KEY;
|
|
414
|
+
if (settlerKey) {
|
|
415
|
+
try {
|
|
416
|
+
const provider = new ethers.JsonRpcProvider(this.rpcUrl);
|
|
417
|
+
this.settlerWallet = new ethers.Wallet(settlerKey, provider);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.warn("[TempoFacilitator] Invalid TEMPO_SETTLER_KEY, permit settlement disabled:", err);
|
|
420
|
+
this.settlerWallet = null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Settler EOA address advertised to clients via `X-Payment-Required.extra.tempoSpender`.
|
|
426
|
+
* Web Client uses this as the `spender` field in the signed EIP-2612 Permit.
|
|
427
|
+
* Returns null if no TEMPO_SETTLER_KEY is configured — permit settlement unavailable.
|
|
428
|
+
*/
|
|
429
|
+
getSpenderAddress() {
|
|
430
|
+
return this.settlerWallet?.address ?? null;
|
|
405
431
|
}
|
|
406
432
|
async healthCheck() {
|
|
407
433
|
const start = Date.now();
|
|
@@ -427,6 +453,44 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
427
453
|
}
|
|
428
454
|
}
|
|
429
455
|
async verify(paymentPayload, requirements) {
|
|
456
|
+
const inner = paymentPayload.payload;
|
|
457
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
458
|
+
return this.verifyPermit(inner, requirements);
|
|
459
|
+
}
|
|
460
|
+
return this.verifyTxHash(paymentPayload, requirements);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Structural validation of an EIP-2612 permit payload. Does NOT submit
|
|
464
|
+
* anything on-chain — actual submission happens in settlePermit().
|
|
465
|
+
*/
|
|
466
|
+
async verifyPermit(payload, requirements) {
|
|
467
|
+
if (!this.settlerWallet) {
|
|
468
|
+
return { valid: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
469
|
+
}
|
|
470
|
+
const p = payload.permit;
|
|
471
|
+
if (!p || !p.owner || !p.spender || !p.value || !p.deadline) {
|
|
472
|
+
return { valid: false, error: "Invalid permit payload: missing fields" };
|
|
473
|
+
}
|
|
474
|
+
if (p.spender.toLowerCase() !== this.settlerWallet.address.toLowerCase()) {
|
|
475
|
+
return {
|
|
476
|
+
valid: false,
|
|
477
|
+
error: `Permit spender ${p.spender} does not match configured settler ${this.settlerWallet.address}`
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const deadline = BigInt(p.deadline);
|
|
481
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
482
|
+
if (deadline <= now) {
|
|
483
|
+
return { valid: false, error: "Permit deadline has expired" };
|
|
484
|
+
}
|
|
485
|
+
if (BigInt(p.value) < BigInt(requirements.amount || "0")) {
|
|
486
|
+
return {
|
|
487
|
+
valid: false,
|
|
488
|
+
error: `Permit value ${p.value} is less than required ${requirements.amount}`
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return { valid: true, details: { scheme: "permit", owner: p.owner } };
|
|
492
|
+
}
|
|
493
|
+
async verifyTxHash(paymentPayload, requirements) {
|
|
430
494
|
try {
|
|
431
495
|
const tempoPayload = paymentPayload.payload;
|
|
432
496
|
if (!tempoPayload?.txHash) {
|
|
@@ -484,7 +548,11 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
484
548
|
}
|
|
485
549
|
}
|
|
486
550
|
async settle(paymentPayload, requirements) {
|
|
487
|
-
const
|
|
551
|
+
const inner = paymentPayload.payload;
|
|
552
|
+
if (inner && "permit" in inner && inner.permit) {
|
|
553
|
+
return this.settlePermit(inner, requirements);
|
|
554
|
+
}
|
|
555
|
+
const verifyResult = await this.verifyTxHash(paymentPayload, requirements);
|
|
488
556
|
if (!verifyResult.valid) {
|
|
489
557
|
return { success: false, error: verifyResult.error };
|
|
490
558
|
}
|
|
@@ -495,6 +563,52 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
495
563
|
status: "settled"
|
|
496
564
|
};
|
|
497
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* EIP-2612 permit settlement path. Submits two transactions on Tempo:
|
|
568
|
+
* 1. pathUSD.permit(owner, spender=settler, value, deadline, v, r, s)
|
|
569
|
+
* 2. pathUSD.transferFrom(owner, payTo, value)
|
|
570
|
+
*
|
|
571
|
+
* The settler EOA pays Tempo gas (via the TIP-20 `feeToken` mechanism — no
|
|
572
|
+
* native tTEMPO required; any held TIP-20 token balance covers fees).
|
|
573
|
+
*/
|
|
574
|
+
async settlePermit(payload, requirements) {
|
|
575
|
+
if (!this.settlerWallet) {
|
|
576
|
+
return { success: false, error: "Permit settlement not configured (TEMPO_SETTLER_KEY missing)" };
|
|
577
|
+
}
|
|
578
|
+
if (!requirements.asset || !requirements.payTo) {
|
|
579
|
+
return { success: false, error: "Missing asset or payTo in requirements" };
|
|
580
|
+
}
|
|
581
|
+
const verifyResult = await this.verifyPermit(payload, requirements);
|
|
582
|
+
if (!verifyResult.valid) {
|
|
583
|
+
return { success: false, error: verifyResult.error };
|
|
584
|
+
}
|
|
585
|
+
const token = new ethers.Contract(requirements.asset, TIP20_PERMIT_ABI, this.settlerWallet);
|
|
586
|
+
const p = payload.permit;
|
|
587
|
+
try {
|
|
588
|
+
const permitTx = await token.permit(
|
|
589
|
+
p.owner,
|
|
590
|
+
p.spender,
|
|
591
|
+
p.value,
|
|
592
|
+
p.deadline,
|
|
593
|
+
p.v,
|
|
594
|
+
p.r,
|
|
595
|
+
p.s
|
|
596
|
+
);
|
|
597
|
+
await permitTx.wait();
|
|
598
|
+
const transferTx = await token.transferFrom(p.owner, requirements.payTo, p.value);
|
|
599
|
+
await transferTx.wait();
|
|
600
|
+
return {
|
|
601
|
+
success: true,
|
|
602
|
+
transaction: transferTx.hash,
|
|
603
|
+
status: "settled"
|
|
604
|
+
};
|
|
605
|
+
} catch (err) {
|
|
606
|
+
return {
|
|
607
|
+
success: false,
|
|
608
|
+
error: `Tempo permit settlement failed: ${err.message}`
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
498
612
|
async getTransactionReceipt(txHash) {
|
|
499
613
|
const response = await fetch(this.rpcUrl, {
|
|
500
614
|
method: "POST",
|
|
@@ -737,12 +851,12 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
737
851
|
return this.spenderAddress;
|
|
738
852
|
}
|
|
739
853
|
async getServerAddress() {
|
|
740
|
-
const { ethers } = await import("ethers");
|
|
741
|
-
const wallet = new
|
|
854
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
855
|
+
const wallet = new ethers2.Wallet(this.serverPrivateKey);
|
|
742
856
|
return wallet.address;
|
|
743
857
|
}
|
|
744
858
|
async recoverIntentSigner(intent, chainId) {
|
|
745
|
-
const { ethers } = await import("ethers");
|
|
859
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
746
860
|
const domain = {
|
|
747
861
|
...EIP712_DOMAIN,
|
|
748
862
|
chainId
|
|
@@ -756,7 +870,7 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
756
870
|
nonce: intent.nonce,
|
|
757
871
|
deadline: intent.deadline
|
|
758
872
|
};
|
|
759
|
-
const recoveredAddress =
|
|
873
|
+
const recoveredAddress = ethers2.verifyTypedData(
|
|
760
874
|
domain,
|
|
761
875
|
INTENT_TYPES,
|
|
762
876
|
message,
|
|
@@ -800,10 +914,10 @@ var BNBFacilitator = class extends BaseFacilitator {
|
|
|
800
914
|
return result.result || "0x0";
|
|
801
915
|
}
|
|
802
916
|
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
803
|
-
const { ethers } = await import("ethers");
|
|
804
|
-
const provider = new
|
|
805
|
-
const wallet = new
|
|
806
|
-
const tokenContract = new
|
|
917
|
+
const { ethers: ethers2 } = await import("ethers");
|
|
918
|
+
const provider = new ethers2.JsonRpcProvider(rpcUrl);
|
|
919
|
+
const wallet = new ethers2.Wallet(this.serverPrivateKey, provider);
|
|
920
|
+
const tokenContract = new ethers2.Contract(token, [
|
|
807
921
|
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
808
922
|
], wallet);
|
|
809
923
|
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
@@ -1058,16 +1172,16 @@ var SolanaFacilitator = class extends BaseFacilitator {
|
|
|
1058
1172
|
return this.supportedNetworks.includes(network);
|
|
1059
1173
|
}
|
|
1060
1174
|
};
|
|
1061
|
-
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
1175
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey, connection) {
|
|
1062
1176
|
const chainConfig = SOLANA_CHAINS[chain];
|
|
1063
|
-
const
|
|
1177
|
+
const conn = connection ?? new Connection2(chainConfig.rpc, "confirmed");
|
|
1064
1178
|
const mint = new PublicKey2(chainConfig.tokens.USDC.mint);
|
|
1065
1179
|
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
1066
1180
|
const senderATA = await getAssociatedTokenAddress(mint, senderPubkey);
|
|
1067
1181
|
const recipientATA = await getAssociatedTokenAddress(mint, recipientPubkey);
|
|
1068
1182
|
const transaction = new Transaction();
|
|
1069
1183
|
try {
|
|
1070
|
-
await getAccount(
|
|
1184
|
+
await getAccount(conn, recipientATA);
|
|
1071
1185
|
} catch {
|
|
1072
1186
|
transaction.add(
|
|
1073
1187
|
createAssociatedTokenAccountInstruction(
|
|
@@ -1098,12 +1212,371 @@ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amo
|
|
|
1098
1212
|
// decimals
|
|
1099
1213
|
)
|
|
1100
1214
|
);
|
|
1101
|
-
const { blockhash, lastValidBlockHeight } = await
|
|
1215
|
+
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash();
|
|
1102
1216
|
transaction.recentBlockhash = blockhash;
|
|
1103
1217
|
transaction.feePayer = actualFeePayer;
|
|
1104
1218
|
return transaction;
|
|
1105
1219
|
}
|
|
1106
1220
|
|
|
1221
|
+
// src/facilitators/alipay.ts
|
|
1222
|
+
import crypto2 from "crypto";
|
|
1223
|
+
|
|
1224
|
+
// src/facilitators/alipay/rsa2.ts
|
|
1225
|
+
import crypto from "crypto";
|
|
1226
|
+
function rsa2Sign(data, privateKeyPem) {
|
|
1227
|
+
const signer = crypto.createSign("RSA-SHA256");
|
|
1228
|
+
signer.update(data, "utf-8");
|
|
1229
|
+
signer.end();
|
|
1230
|
+
return signer.sign(privateKeyPem, "base64");
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/facilitators/alipay/encoding.ts
|
|
1234
|
+
function base64url(input) {
|
|
1235
|
+
return Buffer.from(input, "utf-8").toString("base64url");
|
|
1236
|
+
}
|
|
1237
|
+
function decodeBase64UrlWithPadFix(input) {
|
|
1238
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1239
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
1240
|
+
return Buffer.from(padded, "base64").toString("utf-8");
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/facilitators/alipay/openapi.ts
|
|
1244
|
+
function formatAlipayTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
1245
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1246
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1247
|
+
}
|
|
1248
|
+
function buildSigningString(params) {
|
|
1249
|
+
return Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
1250
|
+
}
|
|
1251
|
+
function responseWrapperKey(method) {
|
|
1252
|
+
return `${method.replace(/\./g, "_")}_response`;
|
|
1253
|
+
}
|
|
1254
|
+
async function alipayOpenApiCall(method, bizContent, config) {
|
|
1255
|
+
const publicParams = {
|
|
1256
|
+
app_id: config.app_id,
|
|
1257
|
+
method,
|
|
1258
|
+
format: "JSON",
|
|
1259
|
+
charset: "utf-8",
|
|
1260
|
+
sign_type: config.sign_type ?? "RSA2",
|
|
1261
|
+
timestamp: formatAlipayTimestamp(),
|
|
1262
|
+
version: "1.0",
|
|
1263
|
+
biz_content: JSON.stringify(bizContent)
|
|
1264
|
+
};
|
|
1265
|
+
const signingString = buildSigningString(publicParams);
|
|
1266
|
+
const sign = rsa2Sign(signingString, config.private_key_pem);
|
|
1267
|
+
const body = new URLSearchParams({ ...publicParams, sign }).toString();
|
|
1268
|
+
const response = await fetch(config.gateway_url, {
|
|
1269
|
+
method: "POST",
|
|
1270
|
+
headers: {
|
|
1271
|
+
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
|
|
1272
|
+
},
|
|
1273
|
+
body
|
|
1274
|
+
});
|
|
1275
|
+
if (!response.ok) {
|
|
1276
|
+
const text = await response.text().catch(() => "<unreadable>");
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
`Alipay Open API HTTP ${response.status} for ${method}: ${text.slice(0, 500)}`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
const json = await response.json();
|
|
1282
|
+
const wrapperKey = responseWrapperKey(method);
|
|
1283
|
+
const business = json[wrapperKey];
|
|
1284
|
+
if (business === void 0 || business === null || typeof business !== "object") {
|
|
1285
|
+
throw new Error(
|
|
1286
|
+
`Alipay Open API response missing "${wrapperKey}" wrapper for ${method}: ${JSON.stringify(json).slice(0, 500)}`
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
return business;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// src/facilitators/alipay.ts
|
|
1293
|
+
var ALIPAY_NETWORK = "alipay";
|
|
1294
|
+
var ALIPAY_SCHEME = "alipay-aipay";
|
|
1295
|
+
var ALIPAY_GATEWAY_PROD = "https://openapi.alipay.com/gateway.do";
|
|
1296
|
+
var ALIPAY_GATEWAY_SANDBOX = "https://openapi.alipaydev.com/gateway.do";
|
|
1297
|
+
var ALIPAY_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
|
|
1298
|
+
var ALIPAY_PAY_BEFORE_MS = 30 * 60 * 1e3;
|
|
1299
|
+
var ALIPAY_SIGNING_FIELDS = [
|
|
1300
|
+
"amount",
|
|
1301
|
+
"currency",
|
|
1302
|
+
"goods_name",
|
|
1303
|
+
"out_trade_no",
|
|
1304
|
+
"pay_before",
|
|
1305
|
+
"resource_id",
|
|
1306
|
+
"seller_id",
|
|
1307
|
+
"service_id"
|
|
1308
|
+
];
|
|
1309
|
+
var AlipayFacilitator = class extends BaseFacilitator {
|
|
1310
|
+
name = "alipay";
|
|
1311
|
+
displayName = "Alipay AI \u6536";
|
|
1312
|
+
supportedNetworks = [ALIPAY_NETWORK];
|
|
1313
|
+
config;
|
|
1314
|
+
constructor(config) {
|
|
1315
|
+
super();
|
|
1316
|
+
this.config = {
|
|
1317
|
+
gateway_url: ALIPAY_GATEWAY_PROD,
|
|
1318
|
+
sign_type: "RSA2",
|
|
1319
|
+
...config
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Build the 402 challenge for a service: signs the 8-field payload with
|
|
1324
|
+
* RSA2, packages the nested `{protocol, method}` JSON as Base64URL for
|
|
1325
|
+
* `Payment-Needed`, and emits the parallel x402 `accepts[]` entry.
|
|
1326
|
+
*/
|
|
1327
|
+
async createPaymentRequirements(opts) {
|
|
1328
|
+
if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
|
|
1329
|
+
throw new Error(
|
|
1330
|
+
`AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
const now = /* @__PURE__ */ new Date();
|
|
1334
|
+
const outTradeNo = opts.outTradeNo ?? generateOutTradeNo();
|
|
1335
|
+
const payBefore = formatPayBefore(now);
|
|
1336
|
+
const signedFields = {
|
|
1337
|
+
amount: opts.priceCny,
|
|
1338
|
+
currency: "CNY",
|
|
1339
|
+
goods_name: opts.goodsName,
|
|
1340
|
+
out_trade_no: outTradeNo,
|
|
1341
|
+
pay_before: payBefore,
|
|
1342
|
+
resource_id: opts.resourceId,
|
|
1343
|
+
seller_id: this.config.seller_id,
|
|
1344
|
+
service_id: opts.serviceId
|
|
1345
|
+
};
|
|
1346
|
+
const signingString = ALIPAY_SIGNING_FIELDS.map((k) => `${k}=${signedFields[k]}`).join("&");
|
|
1347
|
+
const seller_signature = rsa2Sign(signingString, this.config.private_key_pem);
|
|
1348
|
+
const challenge = {
|
|
1349
|
+
protocol: {
|
|
1350
|
+
out_trade_no: outTradeNo,
|
|
1351
|
+
amount: signedFields.amount,
|
|
1352
|
+
currency: signedFields.currency,
|
|
1353
|
+
resource_id: signedFields.resource_id,
|
|
1354
|
+
pay_before: payBefore,
|
|
1355
|
+
seller_signature,
|
|
1356
|
+
seller_sign_type: this.config.sign_type ?? "RSA2",
|
|
1357
|
+
seller_unique_id: this.config.seller_id
|
|
1358
|
+
},
|
|
1359
|
+
method: {
|
|
1360
|
+
seller_name: this.config.seller_name,
|
|
1361
|
+
seller_id: this.config.seller_id,
|
|
1362
|
+
seller_app_id: this.config.app_id,
|
|
1363
|
+
goods_name: signedFields.goods_name,
|
|
1364
|
+
seller_unique_id_key: "seller_id",
|
|
1365
|
+
service_id: signedFields.service_id
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
const paymentNeededHeader = base64url(JSON.stringify(challenge));
|
|
1369
|
+
const x402Accepts = {
|
|
1370
|
+
scheme: ALIPAY_SCHEME,
|
|
1371
|
+
network: ALIPAY_NETWORK,
|
|
1372
|
+
asset: "CNY",
|
|
1373
|
+
amount: opts.priceCny,
|
|
1374
|
+
payTo: this.config.seller_id,
|
|
1375
|
+
maxTimeoutSeconds: ALIPAY_PAY_BEFORE_MS / 1e3,
|
|
1376
|
+
extra: {
|
|
1377
|
+
payment_needed_header: paymentNeededHeader,
|
|
1378
|
+
out_trade_no: outTradeNo,
|
|
1379
|
+
pay_before: payBefore,
|
|
1380
|
+
service_id: signedFields.service_id
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
return { x402Accepts, paymentNeededHeader };
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Verify a `Payment-Proof` by calling
|
|
1387
|
+
* `alipay.aipay.agent.payment.verify` against the Alipay Open API.
|
|
1388
|
+
*
|
|
1389
|
+
* The proof is conveyed via `paymentPayload.payload`, which may be:
|
|
1390
|
+
* - a raw Base64URL string (the `Payment-Proof` header value), or
|
|
1391
|
+
* - an object `{ paymentProof: string }` / `{ proofHeader: string }`.
|
|
1392
|
+
*
|
|
1393
|
+
* All failure modes (malformed payload, network errors, Alipay
|
|
1394
|
+
* `code != 10000`) return `{ valid: false, error }`. No exception
|
|
1395
|
+
* escapes, regardless of client-supplied input.
|
|
1396
|
+
*/
|
|
1397
|
+
async verify(paymentPayload, _requirements) {
|
|
1398
|
+
try {
|
|
1399
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1400
|
+
const decoded = decodeProof(proofHeader);
|
|
1401
|
+
const response = await alipayOpenApiCall(
|
|
1402
|
+
"alipay.aipay.agent.payment.verify",
|
|
1403
|
+
{
|
|
1404
|
+
payment_proof: decoded.protocol.payment_proof,
|
|
1405
|
+
trade_no: decoded.protocol.trade_no,
|
|
1406
|
+
client_session: decoded.method.client_session
|
|
1407
|
+
},
|
|
1408
|
+
this.getOpenApiConfig()
|
|
1409
|
+
);
|
|
1410
|
+
if (response.code !== "10000") {
|
|
1411
|
+
return {
|
|
1412
|
+
valid: false,
|
|
1413
|
+
error: `alipay verify ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1414
|
+
details: {
|
|
1415
|
+
code: response.code,
|
|
1416
|
+
sub_code: response.sub_code,
|
|
1417
|
+
sub_msg: response.sub_msg
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
return {
|
|
1422
|
+
valid: true,
|
|
1423
|
+
details: {
|
|
1424
|
+
trade_no: response.trade_no ?? decoded.protocol.trade_no,
|
|
1425
|
+
amount: response.amount,
|
|
1426
|
+
out_trade_no: response.out_trade_no,
|
|
1427
|
+
resource_id: response.resource_id,
|
|
1428
|
+
active: response.active
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
} catch (e) {
|
|
1432
|
+
return {
|
|
1433
|
+
valid: false,
|
|
1434
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Settle by calling `alipay.aipay.agent.fulfillment.confirm` after the
|
|
1440
|
+
* service resource has been returned to the buyer.
|
|
1441
|
+
*
|
|
1442
|
+
* Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
|
|
1443
|
+
* "履约确认失败"), this is **fire-and-forget**: the caller (registry /
|
|
1444
|
+
* server) is expected to log fulfillment failures but NOT roll back
|
|
1445
|
+
* the already-delivered resource.
|
|
1446
|
+
*
|
|
1447
|
+
* Re-decodes `paymentPayload.payload` to extract `trade_no` (verify
|
|
1448
|
+
* does the same; the redundant Base64URL decode is negligible).
|
|
1449
|
+
*/
|
|
1450
|
+
async settle(paymentPayload, _requirements) {
|
|
1451
|
+
try {
|
|
1452
|
+
const proofHeader = extractProofHeader(paymentPayload.payload);
|
|
1453
|
+
const decoded = decodeProof(proofHeader);
|
|
1454
|
+
const tradeNo = decoded.protocol.trade_no;
|
|
1455
|
+
const response = await alipayOpenApiCall(
|
|
1456
|
+
"alipay.aipay.agent.fulfillment.confirm",
|
|
1457
|
+
{ trade_no: tradeNo },
|
|
1458
|
+
this.getOpenApiConfig()
|
|
1459
|
+
);
|
|
1460
|
+
if (response.code !== "10000") {
|
|
1461
|
+
return {
|
|
1462
|
+
success: false,
|
|
1463
|
+
transaction: tradeNo,
|
|
1464
|
+
error: `alipay fulfillment ${response.code}: ${response.sub_msg ?? response.msg ?? "unknown"}`,
|
|
1465
|
+
status: "fulfillment_failed"
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
success: true,
|
|
1470
|
+
transaction: tradeNo,
|
|
1471
|
+
status: "fulfilled"
|
|
1472
|
+
};
|
|
1473
|
+
} catch (e) {
|
|
1474
|
+
return {
|
|
1475
|
+
success: false,
|
|
1476
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Validate that the configured RSA keys parse and that the Open API
|
|
1482
|
+
* gateway is reachable. Does NOT make a real business API call (would
|
|
1483
|
+
* burn quota and could appear as a legitimate verify attempt in logs).
|
|
1484
|
+
*/
|
|
1485
|
+
async healthCheck() {
|
|
1486
|
+
const start = Date.now();
|
|
1487
|
+
try {
|
|
1488
|
+
crypto2.createPrivateKey(this.config.private_key_pem);
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
return {
|
|
1491
|
+
healthy: false,
|
|
1492
|
+
error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
try {
|
|
1496
|
+
crypto2.createPublicKey(this.config.alipay_public_key_pem);
|
|
1497
|
+
} catch (e) {
|
|
1498
|
+
return {
|
|
1499
|
+
healthy: false,
|
|
1500
|
+
error: `alipay_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
const gatewayUrl = this.config.gateway_url ?? ALIPAY_GATEWAY_PROD;
|
|
1504
|
+
const controller = new AbortController();
|
|
1505
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
1506
|
+
const response = await fetch(gatewayUrl, {
|
|
1507
|
+
method: "HEAD",
|
|
1508
|
+
signal: controller.signal
|
|
1509
|
+
}).catch(() => null);
|
|
1510
|
+
clearTimeout(timeout);
|
|
1511
|
+
const latencyMs = Date.now() - start;
|
|
1512
|
+
if (!response) {
|
|
1513
|
+
return { healthy: false, error: `gateway unreachable: ${gatewayUrl}`, latencyMs };
|
|
1514
|
+
}
|
|
1515
|
+
return { healthy: true, latencyMs };
|
|
1516
|
+
}
|
|
1517
|
+
/** Bundle the facilitator config into the shape openapi.ts wants. */
|
|
1518
|
+
getOpenApiConfig() {
|
|
1519
|
+
return {
|
|
1520
|
+
gateway_url: this.config.gateway_url ?? ALIPAY_GATEWAY_PROD,
|
|
1521
|
+
app_id: this.config.app_id,
|
|
1522
|
+
private_key_pem: this.config.private_key_pem,
|
|
1523
|
+
alipay_public_key_pem: this.config.alipay_public_key_pem,
|
|
1524
|
+
sign_type: this.config.sign_type
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
function generateOutTradeNo() {
|
|
1529
|
+
return "VID" + crypto2.randomBytes(22).toString("base64url").slice(0, 29);
|
|
1530
|
+
}
|
|
1531
|
+
function formatPayBefore(now) {
|
|
1532
|
+
const expiry = new Date(now.getTime() + ALIPAY_PAY_BEFORE_MS);
|
|
1533
|
+
return expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1534
|
+
}
|
|
1535
|
+
function extractProofHeader(payload) {
|
|
1536
|
+
if (typeof payload === "string") {
|
|
1537
|
+
if (payload.length === 0) {
|
|
1538
|
+
throw new Error("alipay Payment-Proof is empty");
|
|
1539
|
+
}
|
|
1540
|
+
return payload;
|
|
1541
|
+
}
|
|
1542
|
+
if (payload !== null && typeof payload === "object") {
|
|
1543
|
+
const obj = payload;
|
|
1544
|
+
const candidate = obj.paymentProof ?? obj.proofHeader;
|
|
1545
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
1546
|
+
return candidate;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
throw new Error(
|
|
1550
|
+
"alipay payment payload must be a Base64URL string or {paymentProof: string} / {proofHeader: string}"
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
function decodeProof(proofHeader) {
|
|
1554
|
+
let parsed;
|
|
1555
|
+
try {
|
|
1556
|
+
parsed = JSON.parse(decodeBase64UrlWithPadFix(proofHeader));
|
|
1557
|
+
} catch (e) {
|
|
1558
|
+
throw new Error(
|
|
1559
|
+
`failed to decode Payment-Proof: ${e instanceof Error ? e.message : String(e)}`
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1563
|
+
throw new Error("decoded Payment-Proof is not an object");
|
|
1564
|
+
}
|
|
1565
|
+
const obj = parsed;
|
|
1566
|
+
const protocol = obj.protocol;
|
|
1567
|
+
const method = obj.method;
|
|
1568
|
+
if (!protocol || typeof protocol.payment_proof !== "string") {
|
|
1569
|
+
throw new Error("decoded Payment-Proof missing protocol.payment_proof");
|
|
1570
|
+
}
|
|
1571
|
+
if (typeof protocol.trade_no !== "string") {
|
|
1572
|
+
throw new Error("decoded Payment-Proof missing protocol.trade_no");
|
|
1573
|
+
}
|
|
1574
|
+
if (!method || typeof method.client_session !== "string") {
|
|
1575
|
+
throw new Error("decoded Payment-Proof missing method.client_session");
|
|
1576
|
+
}
|
|
1577
|
+
return parsed;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1107
1580
|
// src/facilitators/registry.ts
|
|
1108
1581
|
import { Keypair as Keypair2 } from "@solana/web3.js";
|
|
1109
1582
|
import bs58 from "bs58";
|
|
@@ -1128,6 +1601,7 @@ var FacilitatorRegistry = class {
|
|
|
1128
1601
|
}
|
|
1129
1602
|
return new SolanaFacilitator({ feePayerKeypair });
|
|
1130
1603
|
});
|
|
1604
|
+
this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
|
|
1131
1605
|
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
1132
1606
|
}
|
|
1133
1607
|
/**
|
|
@@ -1354,6 +1828,11 @@ function createRegistry(selection) {
|
|
|
1354
1828
|
return new FacilitatorRegistry(selection);
|
|
1355
1829
|
}
|
|
1356
1830
|
export {
|
|
1831
|
+
ALIPAY_GATEWAY_PROD,
|
|
1832
|
+
ALIPAY_GATEWAY_SANDBOX,
|
|
1833
|
+
ALIPAY_NETWORK,
|
|
1834
|
+
ALIPAY_SCHEME,
|
|
1835
|
+
AlipayFacilitator,
|
|
1357
1836
|
BNBFacilitator,
|
|
1358
1837
|
BaseFacilitator,
|
|
1359
1838
|
CDPFacilitator,
|