tantk commited on
Commit
6cce677
Β·
1 Parent(s): 3f34196

fix: extract payer from Soroban auth entries, synchronous refund in handler

Browse files
Files changed (1) hide show
  1. server.js +39 -51
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
 
@@ -35,27 +36,26 @@ function isAllowedUrl(urlStr) {
35
  }
36
  }
37
 
38
- // Extract payer from the PAYMENT-RESPONSE header (set by x402 after settlement)
39
- function getPayerFromSettlement(res) {
40
  try {
41
- // Try multiple header name cases β€” Express normalizes to lowercase
42
- const header =
43
- res.getHeader("PAYMENT-RESPONSE") ||
44
- res.getHeader("payment-response") ||
45
- res.getHeader("Payment-Response");
46
- if (!header) {
47
- // Log all response headers for debugging
48
- const allHeaders = res.getHeaders();
49
- const paymentHeaders = Object.keys(allHeaders).filter(
50
- (k) => k.toLowerCase().includes("payment"),
51
- );
52
- console.log("Payment-related response headers:", paymentHeaders);
53
- return null;
 
54
  }
55
- const decoded = JSON.parse(Buffer.from(String(header), "base64").toString());
56
- return decoded?.payer || null;
57
- } catch (err) {
58
- console.error("Payer extraction from settlement failed:", err.message);
59
  return null;
60
  }
61
  }
@@ -95,27 +95,6 @@ app.use("/render", (req, res, next) => {
95
  next();
96
  });
97
 
98
- // Post-settlement refund hook β€” attach finish listener before x402 middleware
99
- app.use("/render", (req, res, next) => {
100
- res.on("finish", async () => {
101
- if (!res.locals.needsRefund) return;
102
-
103
- const payer = getPayerFromSettlement(res);
104
- if (!payer) {
105
- console.log("Refund needed but could not extract payer address");
106
- return;
107
- }
108
-
109
- const reason = res.locals.failReason || "unknown";
110
- console.log(`Issuing refund to ${payer} β€” reason: ${reason}`);
111
- const hash = await sendRefund(payer, REFUND_AMOUNT, `refund:${reason}`);
112
- if (hash) {
113
- console.log(`Refund sent: ${hash}`);
114
- }
115
- });
116
- next();
117
- });
118
-
119
  // x402 payment middleware β€” protects /render
120
  app.use(
121
  paymentMiddlewareFromConfig(
@@ -148,18 +127,21 @@ app.get("/render", async (req, res) => {
148
 
149
  const failReason = isFailedRender(result.content, result.title);
150
  if (failReason) {
151
- // Tag the response so post-settlement hook knows to refund
152
- res.locals.needsRefund = true;
153
- res.locals.failReason = failReason;
 
 
 
 
 
 
154
 
155
  return res.json({
156
  ...result,
157
  renderTimeMs: elapsed,
158
  payment: { price: PRICE, network: NETWORK },
159
- renderFailed: {
160
- reason: failReason,
161
- message: "Page was blocked or empty β€” refund will be issued",
162
- },
163
  });
164
  }
165
 
@@ -173,10 +155,16 @@ app.get("/render", async (req, res) => {
173
  if (err.message.includes("Too many concurrent")) {
174
  return res.status(503).json({ error: err.message });
175
  }
176
- // Tag for refund on crash too
177
- res.locals.needsRefund = true;
178
- res.locals.failReason = "render_crash";
179
- res.status(500).json({ error: "Render failed", message: err.message });
 
 
 
 
 
 
180
  }
181
  });
182
 
 
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, Address } from "@stellar/stellar-sdk";
7
  import { renderUrl, closeBrowser } from "./renderer.js";
8
  import { isFailedRender, sendRefund } from "./refund.js";
9
 
 
36
  }
37
  }
38
 
39
+ // Extract payer from the payment-signature request header (Soroban auth entry)
40
+ function getPayerAddress(req) {
41
  try {
42
+ const header = req.get("payment-signature") || req.get("x-payment");
43
+ if (!header) return null;
44
+ const decoded = JSON.parse(Buffer.from(header, "base64").toString());
45
+ const txXdr = decoded?.payload?.transaction;
46
+ if (!txXdr) return null;
47
+ const tx = new Transaction(txXdr, Networks.TESTNET);
48
+ const op = tx.operations[0];
49
+ if (op?.auth) {
50
+ for (const a of op.auth) {
51
+ const creds = a.credentials();
52
+ if (creds.switch().name === "sorobanCredentialsAddress") {
53
+ return Address.fromScAddress(creds.address().address()).toString();
54
+ }
55
+ }
56
  }
57
+ return null;
58
+ } catch {
 
 
59
  return null;
60
  }
61
  }
 
95
  next();
96
  });
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  // x402 payment middleware β€” protects /render
99
  app.use(
100
  paymentMiddlewareFromConfig(
 
127
 
128
  const failReason = isFailedRender(result.content, result.title);
129
  if (failReason) {
130
+ const payerAddress = getPayerAddress(req);
131
+ let refund = null;
132
+ if (payerAddress) {
133
+ console.log(`Bad render (${failReason}) for ${decoded} β€” refunding ${payerAddress}`);
134
+ const refundHash = await sendRefund(payerAddress, REFUND_AMOUNT, `refund:${failReason}`);
135
+ refund = refundHash
136
+ ? { transaction: refundHash, amount: `${REFUND_AMOUNT} USDC`, reason: failReason }
137
+ : { error: "Refund failed β€” contact support", reason: failReason };
138
+ }
139
 
140
  return res.json({
141
  ...result,
142
  renderTimeMs: elapsed,
143
  payment: { price: PRICE, network: NETWORK },
144
+ refund,
 
 
 
145
  });
146
  }
147
 
 
155
  if (err.message.includes("Too many concurrent")) {
156
  return res.status(503).json({ error: err.message });
157
  }
158
+ // Attempt refund on crash
159
+ const payerAddress = getPayerAddress(req);
160
+ let refund = null;
161
+ if (payerAddress) {
162
+ const refundHash = await sendRefund(payerAddress, REFUND_AMOUNT, "refund:render_crash");
163
+ refund = refundHash
164
+ ? { transaction: refundHash, amount: `${REFUND_AMOUNT} USDC`, reason: "render_crash" }
165
+ : { error: "Refund failed β€” contact support" };
166
+ }
167
+ res.status(500).json({ error: "Render failed", message: err.message, refund });
168
  }
169
  });
170