Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
- Marked submodules ai-mcp-pmm-controller, explorer-monorepo, and smom-dbis-138 as dirty to reflect recent changes. - Updated documentation to clarify operator script usage, including dotenv loading and task execution instructions. - Enhanced the README and various index files to provide clearer navigation and task completion guidance. Made-with: Cursor
282 lines
8.6 KiB
JavaScript
282 lines
8.6 KiB
JavaScript
/**
|
|
* Minimal x402-enabled Express API using thirdweb settlePayment.
|
|
* Supports Chain 138, Alltra (651940) with USDC + local verification.
|
|
* See docs/04-configuration/X402_ALLTRA_ENDPOINT_SPEC.md and CHAIN138_X402_TOKEN_SUPPORT.md.
|
|
*/
|
|
import "dotenv/config";
|
|
import express from "express";
|
|
import { createThirdwebClient, defineChain } from "thirdweb";
|
|
import { facilitator, settlePayment } from "thirdweb/x402";
|
|
import { arbitrumSepolia } from "thirdweb/chains";
|
|
import { randomUUID } from "crypto";
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
const PORT = process.env.PORT || 4020;
|
|
const secretKey = process.env.THIRDWEB_SECRET_KEY;
|
|
const serverWalletAddress = process.env.SERVER_WALLET_ADDRESS;
|
|
const useChain138 = process.env.X402_USE_CHAIN_138 === "true";
|
|
const useAlltra = process.env.X402_USE_ALLTRA === "true";
|
|
const rpcUrl138 = process.env.RPC_URL_138 || "https://rpc-http-pub.d-bis.org";
|
|
const rpcUrl651940 = process.env.CHAIN_651940_RPC_URL || process.env.RPC_URL_651940 || "https://mainnet-rpc.alltra.global";
|
|
|
|
/** Custom Chain 138 for thirdweb (DeFi Oracle Meta Mainnet) */
|
|
const chain138 = defineChain({
|
|
id: 138,
|
|
name: "DeFi Oracle Meta Mainnet",
|
|
rpc: rpcUrl138,
|
|
nativeCurrency: {
|
|
name: "Ether",
|
|
symbol: "ETH",
|
|
decimals: 18,
|
|
},
|
|
});
|
|
|
|
/** Chain 651940 — ALL Mainnet (Alltra); default for Alltra-native x402 + USDC */
|
|
const chain651940 = defineChain({
|
|
id: 651940,
|
|
name: "ALL Mainnet",
|
|
rpc: rpcUrl651940,
|
|
nativeCurrency: {
|
|
name: "Ether",
|
|
symbol: "ETH",
|
|
decimals: 18,
|
|
},
|
|
blockExplorers: [
|
|
{ name: "Alltra", url: "https://alltra.global" },
|
|
],
|
|
});
|
|
|
|
const client = secretKey
|
|
? createThirdwebClient({ secretKey })
|
|
: null;
|
|
const thirdwebFacilitator =
|
|
client && serverWalletAddress
|
|
? facilitator({
|
|
client,
|
|
serverWalletAddress,
|
|
})
|
|
: null;
|
|
|
|
/** Resolve network: Alltra (651940) if enabled, else Chain 138 if enabled, else Arbitrum Sepolia for testing. */
|
|
function getNetwork() {
|
|
if (useAlltra && thirdwebFacilitator) {
|
|
return chain651940;
|
|
}
|
|
if (useChain138 && thirdwebFacilitator) {
|
|
return chain138;
|
|
}
|
|
return arbitrumSepolia;
|
|
}
|
|
|
|
/** Alltra USDC (AUSDC) — docs/11-references/ADDRESS_MATRIX_AND_STATUS.md §2.3 */
|
|
const ALLTRA_USDC_ADDRESS = "0xa95EeD79f84E6A0151eaEb9d441F9Ffd50e8e881";
|
|
|
|
/** Replay store: (payer:resourceId:nonce) -> expiresAt (ms). In production use Redis/DB. */
|
|
const replayStore = new Map();
|
|
const REPLAY_TTL_MS = 15 * 60 * 1000; // 15 min
|
|
|
|
function replayKey(payer, resourceId, nonce) {
|
|
return `${payer.toLowerCase()}:${resourceId}:${nonce}`;
|
|
}
|
|
|
|
function isReplayConsumed(payer, resourceId, nonce) {
|
|
const key = replayKey(payer, resourceId, nonce);
|
|
const exp = replayStore.get(key);
|
|
if (!exp) return false;
|
|
if (Date.now() > exp) {
|
|
replayStore.delete(key);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function markReplayConsumed(payer, resourceId, nonce) {
|
|
replayStore.set(replayKey(payer, resourceId, nonce), Date.now() + REPLAY_TTL_MS);
|
|
}
|
|
|
|
/** Price: Alltra USDC, Chain 138 cUSDC, or Arbitrum Sepolia default. */
|
|
function getPrice() {
|
|
if (useAlltra) {
|
|
return {
|
|
amount: "10000",
|
|
asset: { address: ALLTRA_USDC_ADDRESS, decimals: 6 },
|
|
};
|
|
}
|
|
if (useChain138) {
|
|
const cusdc138 = "0xf22258f57794CC8E06237084b353Ab30fFfa640b";
|
|
return {
|
|
amount: "10000",
|
|
asset: { address: cusdc138, decimals: 6 },
|
|
};
|
|
}
|
|
return "$0.01";
|
|
}
|
|
|
|
/** Build PaymentRequired for Alltra (651940) USDC — see X402_ALLTRA_ENDPOINT_SPEC.md */
|
|
function buildPaymentRequired(resourceId) {
|
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
|
return {
|
|
network: "eip155:651940",
|
|
asset: ALLTRA_USDC_ADDRESS,
|
|
amount: "10000",
|
|
recipient: serverWalletAddress,
|
|
nonce: randomUUID(),
|
|
expiresAt,
|
|
resourceId,
|
|
};
|
|
}
|
|
|
|
/** Verify settlement on 651940 via eth_getTransactionReceipt */
|
|
async function verifySettlementOnChain(txHash) {
|
|
const body = JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "eth_getTransactionReceipt",
|
|
params: [txHash],
|
|
});
|
|
const r = await fetch(rpcUrl651940, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body,
|
|
});
|
|
const data = await r.json();
|
|
const receipt = data?.result;
|
|
return receipt && receipt.status === "0x1";
|
|
}
|
|
|
|
/** Alltra-native local verification: 402 + PAYMENT-REQUIRED when unpaid; verify PAYMENT-SIGNATURE and settlement. */
|
|
async function handlePaidRouteAlltra(req, res) {
|
|
if (!serverWalletAddress) {
|
|
return res.status(503).json({
|
|
error: "x402 not configured",
|
|
hint: "Set SERVER_WALLET_ADDRESS in .env",
|
|
});
|
|
}
|
|
|
|
const resourceId = `${req.method} ${req.originalUrl || req.url}`;
|
|
const paymentData =
|
|
req.headers["payment-signature"] ||
|
|
req.headers["PAYMENT-SIGNATURE"] ||
|
|
req.headers["x-payment"] ||
|
|
req.headers["X-PAYMENT"];
|
|
|
|
if (!paymentData || paymentData.trim() === "") {
|
|
const paymentRequired = buildPaymentRequired(resourceId);
|
|
const headerValue = Buffer.from(JSON.stringify(paymentRequired), "utf8").toString("base64");
|
|
return res
|
|
.status(402)
|
|
.set("PAYMENT-REQUIRED", headerValue)
|
|
.json({ error: "Payment required", paymentRequired: { ...paymentRequired, amount: paymentRequired.amount } });
|
|
}
|
|
|
|
let payload;
|
|
try {
|
|
payload = JSON.parse(Buffer.from(paymentData, "base64").toString("utf8"));
|
|
} catch {
|
|
return res.status(400).json({ error: "Invalid PAYMENT-SIGNATURE: not base64 JSON" });
|
|
}
|
|
|
|
const { payer, paymentRequired: pr, txHash } = payload;
|
|
if (!payer || !pr || !txHash) {
|
|
return res.status(400).json({ error: "PAYMENT-SIGNATURE must include payer, paymentRequired, txHash" });
|
|
}
|
|
|
|
if (pr.recipient?.toLowerCase() !== serverWalletAddress?.toLowerCase() || pr.asset?.toLowerCase() !== ALLTRA_USDC_ADDRESS.toLowerCase()) {
|
|
return res.status(400).json({ error: "Payment intent does not match (recipient or asset)" });
|
|
}
|
|
if (new Date(pr.expiresAt) < new Date()) {
|
|
return res.status(400).json({ error: "Payment expired" });
|
|
}
|
|
|
|
if (isReplayConsumed(payer, resourceId, pr.nonce)) {
|
|
return res.status(400).json({ error: "Replay: payment already consumed" });
|
|
}
|
|
|
|
const ok = await verifySettlementOnChain(txHash);
|
|
if (!ok) {
|
|
return res.status(400).json({ error: "Settlement verification failed: invalid or failed tx on 651940" });
|
|
}
|
|
|
|
markReplayConsumed(payer, resourceId, pr.nonce);
|
|
return res.json({
|
|
data: "paid content",
|
|
message: "Payment settled successfully (Alltra local verification)",
|
|
});
|
|
}
|
|
|
|
/** Shared handler for paid routes (PAYMENT-SIGNATURE or X-PAYMENT header) — thirdweb facilitator path. */
|
|
async function handlePaidRoute(req, res) {
|
|
if (useAlltra) {
|
|
return handlePaidRouteAlltra(req, res);
|
|
}
|
|
|
|
const paymentData =
|
|
req.headers["payment-signature"] ||
|
|
req.headers["PAYMENT-SIGNATURE"] ||
|
|
req.headers["x-payment"] ||
|
|
req.headers["X-PAYMENT"];
|
|
|
|
if (!thirdwebFacilitator || !serverWalletAddress) {
|
|
return res.status(503).json({
|
|
error: "x402 not configured",
|
|
hint: "Set THIRDWEB_SECRET_KEY and SERVER_WALLET_ADDRESS in .env",
|
|
});
|
|
}
|
|
|
|
const resourceUrl =
|
|
(req.protocol + "://" + req.get("host") + req.originalUrl) || "";
|
|
const method = req.method;
|
|
|
|
const result = await settlePayment({
|
|
resourceUrl,
|
|
method,
|
|
paymentData: paymentData || undefined,
|
|
payTo: serverWalletAddress,
|
|
network: getNetwork(),
|
|
price: getPrice(),
|
|
facilitator: thirdwebFacilitator,
|
|
routeConfig: {
|
|
description: "Access to paid API content",
|
|
mimeType: "application/json",
|
|
maxTimeoutSeconds: 60 * 60,
|
|
},
|
|
});
|
|
|
|
if (result.status === 200) {
|
|
return res.json({
|
|
data: "paid content",
|
|
message: "Payment settled successfully",
|
|
});
|
|
}
|
|
res
|
|
.status(result.status)
|
|
.set(result.responseHeaders || {})
|
|
.json(result.responseBody ?? { error: "Payment required" });
|
|
}
|
|
|
|
/** Protected routes: require x402 payment (PAYMENT-SIGNATURE or X-PAYMENT header). */
|
|
app.get("/api/premium", handlePaidRoute);
|
|
app.get("/api/paid", handlePaidRoute);
|
|
|
|
/** Health: no payment required. */
|
|
app.get("/health", (req, res) => {
|
|
const chainName = useAlltra ? "alltra-651940" : useChain138 ? "chain138" : "arbitrumSepolia";
|
|
res.json({
|
|
ok: true,
|
|
x402: !!thirdwebFacilitator,
|
|
chain: chainName,
|
|
});
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`x402-api listening on port ${PORT}`);
|
|
if (!thirdwebFacilitator) {
|
|
console.warn("THIRDWEB_SECRET_KEY or SERVER_WALLET_ADDRESS not set; /api/premium will return 503.");
|
|
} else {
|
|
const chainName = useAlltra ? "ALL Mainnet (651940) USDC" : useChain138 ? "Chain 138" : "Arbitrum Sepolia (default USDC)";
|
|
console.log(`Payment chain: ${chainName}`);
|
|
}
|
|
});
|