Spaces:
Sleeping
Sleeping
fix: extract payer from tx XDR, refund on crash, memo length guard, MCP refund info
Browse files- mcp-server.js +1 -1
- refund.js +1 -1
- server.js +24 -6
mcp-server.js
CHANGED
|
@@ -102,7 +102,7 @@ server.tool(
|
|
| 102 |
content: [
|
| 103 |
{
|
| 104 |
type: "text",
|
| 105 |
-
text: `# ${result.title}\n\nURL: ${result.url}\nRendered: ${result.renderedAt}\nRender time: ${result.renderTimeMs}ms\nPayment: ${result.payment?.price} on ${result.payment?.network}\n\n${result.content}`,
|
| 106 |
},
|
| 107 |
],
|
| 108 |
};
|
|
|
|
| 102 |
content: [
|
| 103 |
{
|
| 104 |
type: "text",
|
| 105 |
+
text: `# ${result.title}\n\nURL: ${result.url}\nRendered: ${result.renderedAt}\nRender time: ${result.renderTimeMs}ms\nPayment: ${result.payment?.price} on ${result.payment?.network}${result.refund ? `\nRefund: ${result.refund.amount} — ${result.refund.reason} (tx: ${result.refund.transaction})` : ""}\n\n${result.content}`,
|
| 106 |
},
|
| 107 |
],
|
| 108 |
};
|
refund.js
CHANGED
|
@@ -76,7 +76,7 @@ export async function sendRefund(payerAddress, amount, memo) {
|
|
| 76 |
amount: amount,
|
| 77 |
}),
|
| 78 |
)
|
| 79 |
-
.addMemo(TransactionBuilder.memo("text", memo || "RenderGate refund"))
|
| 80 |
.setTimeout(30)
|
| 81 |
.build();
|
| 82 |
|
|
|
|
| 76 |
amount: amount,
|
| 77 |
}),
|
| 78 |
)
|
| 79 |
+
.addMemo(TransactionBuilder.memo("text", (memo || "RenderGate refund").slice(0, 28)))
|
| 80 |
.setTimeout(30)
|
| 81 |
.build();
|
| 82 |
|
server.js
CHANGED
|
@@ -3,6 +3,7 @@ import express from "express";
|
|
| 3 |
import { paymentMiddlewareFromConfig } from "@x402/express";
|
| 4 |
import { HTTPFacilitatorClient } from "@x402/core/server";
|
| 5 |
import { ExactStellarScheme } from "@x402/stellar/exact/server";
|
|
|
|
| 6 |
import { renderUrl, closeBrowser } from "./renderer.js";
|
| 7 |
import { isFailedRender, sendRefund } from "./refund.js";
|
| 8 |
|
|
@@ -90,13 +91,18 @@ app.use(
|
|
| 90 |
),
|
| 91 |
);
|
| 92 |
|
| 93 |
-
// Extract payer address from the
|
| 94 |
-
function
|
| 95 |
try {
|
| 96 |
-
const header =
|
|
|
|
| 97 |
if (!header) return null;
|
| 98 |
-
const decoded = JSON.parse(Buffer.from(header, "
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
} catch {
|
| 101 |
return null;
|
| 102 |
}
|
|
@@ -114,7 +120,7 @@ app.get("/render", async (req, res) => {
|
|
| 114 |
|
| 115 |
// Check if the render actually succeeded
|
| 116 |
const failReason = isFailedRender(result.content, result.title);
|
| 117 |
-
const payerAddress =
|
| 118 |
if (failReason && payerAddress) {
|
| 119 |
console.log(`Bad render (${failReason}) for ${decoded} — refunding ${payerAddress}`);
|
| 120 |
const refundHash = await sendRefund(payerAddress, "0.001", `refund:${failReason}`);
|
|
@@ -142,6 +148,18 @@ app.get("/render", async (req, res) => {
|
|
| 142 |
if (err.message.includes("Too many concurrent")) {
|
| 143 |
return res.status(503).json({ error: err.message });
|
| 144 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
res.status(500).json({ error: "Render failed", message: err.message });
|
| 146 |
}
|
| 147 |
});
|
|
|
|
| 3 |
import { paymentMiddlewareFromConfig } from "@x402/express";
|
| 4 |
import { HTTPFacilitatorClient } from "@x402/core/server";
|
| 5 |
import { ExactStellarScheme } from "@x402/stellar/exact/server";
|
| 6 |
+
import { Transaction, Networks } from "@stellar/stellar-sdk";
|
| 7 |
import { renderUrl, closeBrowser } from "./renderer.js";
|
| 8 |
import { isFailedRender, sendRefund } from "./refund.js";
|
| 9 |
|
|
|
|
| 91 |
),
|
| 92 |
);
|
| 93 |
|
| 94 |
+
// Extract payer address from the incoming payment-signature request header
|
| 95 |
+
function getPayerAddress(req) {
|
| 96 |
try {
|
| 97 |
+
const header =
|
| 98 |
+
req.get("payment-signature") || req.get("x-payment") || req.get("PAYMENT-SIGNATURE");
|
| 99 |
if (!header) return null;
|
| 100 |
+
const decoded = JSON.parse(Buffer.from(header, "base64url").toString());
|
| 101 |
+
// Decode the transaction XDR to get the source account (payer)
|
| 102 |
+
const txXdr = decoded?.payload?.transaction;
|
| 103 |
+
if (!txXdr) return null;
|
| 104 |
+
const tx = new Transaction(txXdr, Networks.TESTNET);
|
| 105 |
+
return tx.source;
|
| 106 |
} catch {
|
| 107 |
return null;
|
| 108 |
}
|
|
|
|
| 120 |
|
| 121 |
// Check if the render actually succeeded
|
| 122 |
const failReason = isFailedRender(result.content, result.title);
|
| 123 |
+
const payerAddress = getPayerAddress(req);
|
| 124 |
if (failReason && payerAddress) {
|
| 125 |
console.log(`Bad render (${failReason}) for ${decoded} — refunding ${payerAddress}`);
|
| 126 |
const refundHash = await sendRefund(payerAddress, "0.001", `refund:${failReason}`);
|
|
|
|
| 148 |
if (err.message.includes("Too many concurrent")) {
|
| 149 |
return res.status(503).json({ error: err.message });
|
| 150 |
}
|
| 151 |
+
// Attempt refund on crash — agent paid but got nothing
|
| 152 |
+
const payerAddress = getPayerAddress(req);
|
| 153 |
+
if (payerAddress) {
|
| 154 |
+
const refundHash = await sendRefund(payerAddress, "0.001", "refund:render_crash");
|
| 155 |
+
return res.status(500).json({
|
| 156 |
+
error: "Render failed",
|
| 157 |
+
message: err.message,
|
| 158 |
+
refund: refundHash
|
| 159 |
+
? { transaction: refundHash, amount: "0.001 USDC", reason: "render_crash" }
|
| 160 |
+
: { error: "Refund failed — contact support" },
|
| 161 |
+
});
|
| 162 |
+
}
|
| 163 |
res.status(500).json({ error: "Render failed", message: err.message });
|
| 164 |
}
|
| 165 |
});
|