safehands-pharos 1.2.6 → 1.4.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/.env.example +64 -26
- package/README.md +333 -445
- package/dist/cli.d.ts +5 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +124 -98
- package/dist/cli.js.map +1 -1
- package/dist/demo.d.ts +1 -1
- package/dist/demo.js +171 -171
- package/dist/index.d.ts +2 -2
- package/dist/index.js +138 -87
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +1 -1
- package/dist/init.js +65 -65
- package/dist/lib/auditLog.d.ts +9 -0
- package/dist/lib/auditLog.d.ts.map +1 -0
- package/dist/lib/auditLog.js +30 -0
- package/dist/lib/auditLog.js.map +1 -0
- package/dist/lib/constants.d.ts +291 -291
- package/dist/lib/constants.js +292 -292
- package/dist/lib/dodoApi.d.ts +78 -70
- package/dist/lib/dodoApi.d.ts.map +1 -1
- package/dist/lib/dodoApi.js +196 -178
- package/dist/lib/dodoApi.js.map +1 -1
- package/dist/lib/http.d.ts +14 -14
- package/dist/lib/http.js +118 -118
- package/dist/lib/pharosClient.d.ts +58 -58
- package/dist/lib/pharosClient.d.ts.map +1 -1
- package/dist/lib/pharosClient.js +63 -53
- package/dist/lib/pharosClient.js.map +1 -1
- package/dist/lib/policy/actionPolicyEngine.d.ts +53 -53
- package/dist/lib/policy/actionPolicyEngine.js +212 -212
- package/dist/lib/policy/actionPolicyEngine.js.map +1 -1
- package/dist/lib/riskEngine.d.ts +26 -26
- package/dist/lib/riskEngine.js +283 -283
- package/dist/lib/signer/index.d.ts +24 -24
- package/dist/lib/signer/index.d.ts.map +1 -1
- package/dist/lib/signer/index.js +88 -89
- package/dist/lib/signer/index.js.map +1 -1
- package/dist/lib/spendAccumulator.d.ts +10 -0
- package/dist/lib/spendAccumulator.d.ts.map +1 -0
- package/dist/lib/spendAccumulator.js +54 -0
- package/dist/lib/spendAccumulator.js.map +1 -0
- package/dist/lib/testDodoLive.d.ts +1 -1
- package/dist/lib/testDodoLive.js +104 -104
- package/dist/lib/testLiveSafehands.d.ts +1 -1
- package/dist/lib/testLiveSafehands.js +92 -92
- package/dist/lib/testRpc.d.ts +1 -1
- package/dist/lib/testRpc.js +29 -29
- package/dist/lib/testRpcLive.d.ts +1 -1
- package/dist/lib/testRpcLive.js +88 -88
- package/dist/lib/testTools.d.ts +1 -1
- package/dist/lib/testTools.js +397 -397
- package/dist/lib/testX402Live.d.ts +1 -1
- package/dist/lib/testX402Live.js +159 -159
- package/dist/lib/toolResponse.d.ts +25 -25
- package/dist/lib/toolResponse.js +53 -53
- package/dist/lib/wallet/index.d.ts +37 -18
- package/dist/lib/wallet/index.d.ts.map +1 -1
- package/dist/lib/wallet/index.js +128 -70
- package/dist/lib/wallet/index.js.map +1 -1
- package/dist/scripts/checkDeploy.d.ts +1 -1
- package/dist/scripts/checkDeploy.js +24 -24
- package/dist/scripts/deployRegistry.d.ts +1 -1
- package/dist/scripts/deployRegistry.js +100 -100
- package/dist/scripts/testRegistry.d.ts +1 -1
- package/dist/scripts/testRegistry.js +43 -43
- package/dist/tools/approveToken.d.ts +45 -46
- package/dist/tools/approveToken.d.ts.map +1 -1
- package/dist/tools/approveToken.js +85 -83
- package/dist/tools/approveToken.js.map +1 -1
- package/dist/tools/assessRisk.d.ts +79 -79
- package/dist/tools/assessRisk.d.ts.map +1 -1
- package/dist/tools/assessRisk.js +104 -93
- package/dist/tools/assessRisk.js.map +1 -1
- package/dist/tools/checkAllowance.d.ts +43 -36
- package/dist/tools/checkAllowance.d.ts.map +1 -1
- package/dist/tools/checkAllowance.js +56 -42
- package/dist/tools/checkAllowance.js.map +1 -1
- package/dist/tools/checkTokenSecurity.d.ts +46 -46
- package/dist/tools/checkTokenSecurity.d.ts.map +1 -1
- package/dist/tools/checkTokenSecurity.js +95 -88
- package/dist/tools/checkTokenSecurity.js.map +1 -1
- package/dist/tools/createAgentWallet.d.ts +26 -26
- package/dist/tools/createAgentWallet.d.ts.map +1 -1
- package/dist/tools/createAgentWallet.js +58 -59
- package/dist/tools/createAgentWallet.js.map +1 -1
- package/dist/tools/estimateGas.d.ts +79 -79
- package/dist/tools/estimateGas.js +124 -124
- package/dist/tools/executeSwap.d.ts +61 -59
- package/dist/tools/executeSwap.d.ts.map +1 -1
- package/dist/tools/executeSwap.js +141 -129
- package/dist/tools/executeSwap.js.map +1 -1
- package/dist/tools/explainRisk.d.ts +29 -29
- package/dist/tools/explainRisk.js +32 -32
- package/dist/tools/getAgentWallet.d.ts +21 -21
- package/dist/tools/getAgentWallet.js +27 -27
- package/dist/tools/getAgentWalletBalance.d.ts +11 -11
- package/dist/tools/getAgentWalletBalance.js +70 -70
- package/dist/tools/getExecutionHistory.d.ts +49 -51
- package/dist/tools/getExecutionHistory.d.ts.map +1 -1
- package/dist/tools/getExecutionHistory.js +154 -93
- package/dist/tools/getExecutionHistory.js.map +1 -1
- package/dist/tools/getGasPrice.d.ts +43 -43
- package/dist/tools/getGasPrice.js +59 -59
- package/dist/tools/getPoolInfo.d.ts +75 -75
- package/dist/tools/getPoolInfo.js +137 -137
- package/dist/tools/getTokenPrice.d.ts +113 -113
- package/dist/tools/getTokenPrice.js +117 -117
- package/dist/tools/getTransactionStatus.d.ts +43 -57
- package/dist/tools/getTransactionStatus.d.ts.map +1 -1
- package/dist/tools/getTransactionStatus.js +59 -67
- package/dist/tools/getTransactionStatus.js.map +1 -1
- package/dist/tools/getWalletBalance.d.ts +68 -68
- package/dist/tools/getWalletBalance.js +87 -87
- package/dist/tools/publishRiskScore.d.ts +63 -63
- package/dist/tools/publishRiskScore.d.ts.map +1 -1
- package/dist/tools/publishRiskScore.js +88 -85
- package/dist/tools/publishRiskScore.js.map +1 -1
- package/dist/tools/queryRiskRegistry.d.ts +38 -48
- package/dist/tools/queryRiskRegistry.d.ts.map +1 -1
- package/dist/tools/queryRiskRegistry.js +55 -60
- package/dist/tools/queryRiskRegistry.js.map +1 -1
- package/dist/tools/safehandsPreflightCheck.d.ts +77 -77
- package/dist/tools/safehandsPreflightCheck.js +47 -47
- package/dist/tools/safehandsRiskReport.d.ts +81 -81
- package/dist/tools/safehandsRiskReport.js +28 -28
- package/dist/tools/safehandsSafeExecute.d.ts +20 -20
- package/dist/tools/safehandsSafeExecute.d.ts.map +1 -1
- package/dist/tools/safehandsSafeExecute.js +81 -75
- package/dist/tools/safehandsSafeExecute.js.map +1 -1
- package/dist/tools/safehandsWalletHealth.d.ts +14 -14
- package/dist/tools/safehandsWalletHealth.js +103 -103
- package/dist/tools/safehandsX402Preflight.d.ts +26 -26
- package/dist/tools/safehandsX402Preflight.js +65 -65
- package/dist/tools/sendPayment.d.ts +57 -58
- package/dist/tools/sendPayment.d.ts.map +1 -1
- package/dist/tools/sendPayment.js +117 -108
- package/dist/tools/sendPayment.js.map +1 -1
- package/dist/tools/simulateTransaction.d.ts +60 -81
- package/dist/tools/simulateTransaction.d.ts.map +1 -1
- package/dist/tools/simulateTransaction.js +83 -88
- package/dist/tools/simulateTransaction.js.map +1 -1
- package/dist/tools/tokenRegistryStatus.d.ts +26 -26
- package/dist/tools/tokenRegistryStatus.js +96 -96
- package/dist/tools/x402PayAndFetch.d.ts +81 -81
- package/dist/tools/x402PayAndFetch.d.ts.map +1 -1
- package/dist/tools/x402PayAndFetch.js +152 -149
- package/dist/tools/x402PayAndFetch.js.map +1 -1
- package/dist/x402Server.d.ts +1 -1
- package/dist/x402Server.js +252 -252
- package/examples/dashboard/index.html +337 -0
- package/package.json +83 -82
- package/skill/SKILL.md +133 -133
package/dist/x402Server.js
CHANGED
|
@@ -1,253 +1,253 @@
|
|
|
1
|
-
// ─── SafeHands x402 Server ──────────────────────────────────────────────
|
|
2
|
-
// Exposes SafeHands risk endpoints. /supported and /health are free and do
|
|
3
|
-
// not require user private keys. Paid routes are gated only when receiver +
|
|
4
|
-
// facilitator signer config is present.
|
|
5
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
6
|
-
import express from "express";
|
|
7
|
-
import { join } from "path";
|
|
8
|
-
import { readFileSync } from "fs";
|
|
9
|
-
import { defineChain, createWalletClient, http, publicActions } from "viem";
|
|
10
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
11
|
-
import { x402Facilitator } from "@x402/core/facilitator";
|
|
12
|
-
import { toFacilitatorEvmSigner } from "@x402/evm";
|
|
13
|
-
import { ExactEvmScheme as FacilitatorExactEvmScheme } from "@x402/evm/exact/facilitator";
|
|
14
|
-
import { ExactEvmScheme as ServerExactEvmScheme } from "@x402/evm/exact/server";
|
|
15
|
-
import { HTTPFacilitatorClient } from "@x402/core/server";
|
|
16
|
-
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
|
|
17
|
-
import { handleAssessRisk } from "./tools/assessRisk.js";
|
|
18
|
-
import { handleCheckTokenSecurity } from "./tools/checkTokenSecurity.js";
|
|
19
|
-
import { handleSimulateTransaction } from "./tools/simulateTransaction.js";
|
|
20
|
-
import { CHAIN_ID, PHAROS_ENVIRONMENT, RPC_URL, X402_PAYMENT_TOKEN_ADDRESS } from "./lib/constants.js";
|
|
21
|
-
import { fail, ok } from "./lib/toolResponse.js";
|
|
22
|
-
function loadEnv() {
|
|
23
|
-
try {
|
|
24
|
-
const f = readFileSync(join(process.cwd(), ".env"), "utf-8");
|
|
25
|
-
for (const l of f.split("\n")) {
|
|
26
|
-
const t = l.trim();
|
|
27
|
-
if (!t || t.startsWith("#"))
|
|
28
|
-
continue;
|
|
29
|
-
const i = t.indexOf("=");
|
|
30
|
-
if (i === -1)
|
|
31
|
-
continue;
|
|
32
|
-
const key = t.slice(0, i).trim();
|
|
33
|
-
if (process.env[key] === undefined)
|
|
34
|
-
process.env[key] = t.slice(i + 1).trim();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch { }
|
|
38
|
-
}
|
|
39
|
-
loadEnv();
|
|
40
|
-
const receiverAddress = (process.env.X402_PAY_TO || process.env.WALLET_ADDRESS || "");
|
|
41
|
-
const facilitatorPrivateKey = process.env.X402_FACILITATOR_PRIVATE_KEY || "";
|
|
42
|
-
const port = parseInt(process.env.X402_SERVER_PORT || process.env.X402_PORT || "4021", 10);
|
|
43
|
-
const rpcUrl = process.env.PHAROS_RPC_URL || RPC_URL;
|
|
44
|
-
const usdcAddress = X402_PAYMENT_TOKEN_ADDRESS;
|
|
45
|
-
const priceUsdc = process.env.X402_PRICE_USDC || "0.001";
|
|
46
|
-
const isPaidConfigured = Boolean(receiverAddress && facilitatorPrivateKey);
|
|
47
|
-
const pharos = defineChain({
|
|
48
|
-
id: CHAIN_ID,
|
|
49
|
-
name: `Pharos ${PHAROS_ENVIRONMENT}`,
|
|
50
|
-
nativeCurrency: { name: "PHRS", symbol: "PHRS", decimals: 18 },
|
|
51
|
-
rpcUrls: { default: { http: [rpcUrl] } },
|
|
52
|
-
testnet: true,
|
|
53
|
-
});
|
|
54
|
-
const app = express();
|
|
55
|
-
app.disable("x-powered-by");
|
|
56
|
-
app.use((req, res, next) => {
|
|
57
|
-
req.setTimeout(15_000);
|
|
58
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
59
|
-
res.setHeader("Referrer-Policy", "no-referrer");
|
|
60
|
-
next();
|
|
61
|
-
});
|
|
62
|
-
app.use(express.json({ limit: "64kb" }));
|
|
63
|
-
const rateLimitWindowMs = 60_000;
|
|
64
|
-
const rateLimitMax = 120;
|
|
65
|
-
const rateLimitBuckets = new Map();
|
|
66
|
-
app.use((req, res, next) => {
|
|
67
|
-
const key = req.ip || "unknown";
|
|
68
|
-
const now = Date.now();
|
|
69
|
-
const bucket = rateLimitBuckets.get(key);
|
|
70
|
-
if (!bucket || bucket.resetAt <= now) {
|
|
71
|
-
rateLimitBuckets.set(key, { count: 1, resetAt: now + rateLimitWindowMs });
|
|
72
|
-
return next();
|
|
73
|
-
}
|
|
74
|
-
bucket.count += 1;
|
|
75
|
-
if (bucket.count > rateLimitMax) {
|
|
76
|
-
return res.status(429).json(fail("RATE_LIMITED", "Too many requests", true, "x402_server"));
|
|
77
|
-
}
|
|
78
|
-
return next();
|
|
79
|
-
});
|
|
80
|
-
let facilitator = null;
|
|
81
|
-
let resourceServer = null;
|
|
82
|
-
if (isPaidConfigured) {
|
|
83
|
-
const account = privateKeyToAccount(facilitatorPrivateKey);
|
|
84
|
-
const walletClient = createWalletClient({
|
|
85
|
-
account,
|
|
86
|
-
chain: pharos,
|
|
87
|
-
transport: http(undefined, { timeout: 30_000 }),
|
|
88
|
-
}).extend(publicActions);
|
|
89
|
-
const evmSigner = toFacilitatorEvmSigner({
|
|
90
|
-
address: account.address,
|
|
91
|
-
getCode: (args) => walletClient.getCode(args),
|
|
92
|
-
readContract: (args) => walletClient.readContract({ ...args, args: args.args || [] }),
|
|
93
|
-
verifyTypedData: (args) => walletClient.verifyTypedData(args),
|
|
94
|
-
writeContract: (args) => walletClient.writeContract({ ...args, args: args.args || [] }),
|
|
95
|
-
sendTransaction: (args) => walletClient.sendTransaction(args),
|
|
96
|
-
waitForTransactionReceipt: (args) => walletClient.waitForTransactionReceipt(args),
|
|
97
|
-
});
|
|
98
|
-
facilitator = new x402Facilitator();
|
|
99
|
-
facilitator.register(`eip155:${CHAIN_ID}`, new FacilitatorExactEvmScheme(evmSigner, {}));
|
|
100
|
-
const facilitatorClient = new HTTPFacilitatorClient({ url: `http://localhost:${port}` });
|
|
101
|
-
resourceServer = new x402ResourceServer(facilitatorClient);
|
|
102
|
-
const evmScheme = new ServerExactEvmScheme();
|
|
103
|
-
evmScheme.registerMoneyParser(async (amount, network) => {
|
|
104
|
-
if (network === `eip155:${CHAIN_ID}`) {
|
|
105
|
-
return {
|
|
106
|
-
amount: Math.round(amount * 1e6).toString(),
|
|
107
|
-
asset: usdcAddress,
|
|
108
|
-
extra: { token: "USDC", name: "USDC", version: "2" },
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
});
|
|
113
|
-
resourceServer.register(`eip155:${CHAIN_ID}`, evmScheme);
|
|
114
|
-
}
|
|
115
|
-
app.post("/verify", async (req, res) => {
|
|
116
|
-
if (!facilitator) {
|
|
117
|
-
return res.status(503).json(fail("X402_SERVER_RECEIVER_CONFIG_MISSING", "x402 facilitator is not configured. Set X402_PAY_TO/WALLET_ADDRESS and X402_FACILITATOR_PRIVATE_KEY for paid endpoints. Client PRIVATE_KEY is not used for server receiver config.", false, "x402_server"));
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
const { paymentPayload, paymentRequirements } = req.body;
|
|
121
|
-
const result = await facilitator.verify(paymentPayload, paymentRequirements);
|
|
122
|
-
res.json(result);
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
res.status(500).json(fail("X402_PAYMENT_FAILED", e.message, true, "x402_server"));
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
app.post("/settle", async (req, res) => {
|
|
129
|
-
if (!facilitator) {
|
|
130
|
-
return res.status(503).json(fail("X402_SERVER_RECEIVER_CONFIG_MISSING", "x402 facilitator is not configured. Set X402_PAY_TO/WALLET_ADDRESS and X402_FACILITATOR_PRIVATE_KEY for paid endpoints. Client PRIVATE_KEY is not used for server receiver config.", false, "x402_server"));
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
const { paymentPayload, paymentRequirements } = req.body;
|
|
134
|
-
const result = await facilitator.settle(paymentPayload, paymentRequirements);
|
|
135
|
-
res.json(result);
|
|
136
|
-
}
|
|
137
|
-
catch (e) {
|
|
138
|
-
res.status(500).json(fail("X402_PAYMENT_FAILED", e.message, true, "x402_server"));
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
app.get("/supported", (_req, res) => {
|
|
142
|
-
if (facilitator)
|
|
143
|
-
return res.json(facilitator.getSupported());
|
|
144
|
-
return res.json(ok({
|
|
145
|
-
configured: false,
|
|
146
|
-
network: `eip155:${CHAIN_ID}`,
|
|
147
|
-
scheme: "exact",
|
|
148
|
-
asset: usdcAddress,
|
|
149
|
-
priceUsdc,
|
|
150
|
-
freeEndpoints: ["GET /supported", "GET /health"],
|
|
151
|
-
paidEndpoints: ["GET /assess-risk", "GET /check-token-security", "GET /simulate-transaction"],
|
|
152
|
-
configError: {
|
|
153
|
-
code: "X402_SERVER_RECEIVER_CONFIG_MISSING",
|
|
154
|
-
message: "Paid endpoints require receiver/payTo plus facilitator signer config. Free endpoints remain available.",
|
|
155
|
-
},
|
|
156
|
-
}));
|
|
157
|
-
});
|
|
158
|
-
app.get("/health", (_req, res) => {
|
|
159
|
-
res.json(ok({
|
|
160
|
-
status: "ok",
|
|
161
|
-
service: "SafeHands x402 Resource Server",
|
|
162
|
-
network: `Pharos ${PHAROS_ENVIRONMENT} (Chain ID ${CHAIN_ID})`,
|
|
163
|
-
environment: PHAROS_ENVIRONMENT,
|
|
164
|
-
isMainnet: false,
|
|
165
|
-
receiverConfigured: Boolean(receiverAddress),
|
|
166
|
-
paidEndpointsConfigured: isPaidConfigured,
|
|
167
|
-
receiver: receiverAddress || null,
|
|
168
|
-
asset: usdcAddress,
|
|
169
|
-
freeEndpoints: ["/supported", "/health"],
|
|
170
|
-
}));
|
|
171
|
-
});
|
|
172
|
-
function paidConfigError(req, res, next) {
|
|
173
|
-
if (isPaidConfigured)
|
|
174
|
-
return next();
|
|
175
|
-
return res.status(503).json(fail("X402_SERVER_RECEIVER_CONFIG_MISSING", "Paid endpoint receiver/payTo or facilitator signer config is missing. Free endpoints do not require a client signer/private key.", false, "x402_server"));
|
|
176
|
-
}
|
|
177
|
-
if (isPaidConfigured && resourceServer) {
|
|
178
|
-
app.use(paymentMiddleware({
|
|
179
|
-
"GET /assess-risk": {
|
|
180
|
-
accepts: { scheme: "exact", price: priceUsdc, network: `eip155:${CHAIN_ID}`, payTo: receiverAddress },
|
|
181
|
-
description: `Assess transaction risk (USDC ${priceUsdc})`,
|
|
182
|
-
mimeType: "application/json",
|
|
183
|
-
},
|
|
184
|
-
"GET /check-token-security": {
|
|
185
|
-
accepts: { scheme: "exact", price: priceUsdc, network: `eip155:${CHAIN_ID}`, payTo: receiverAddress },
|
|
186
|
-
description: `Verify contract token security profile (USDC ${priceUsdc})`,
|
|
187
|
-
mimeType: "application/json",
|
|
188
|
-
},
|
|
189
|
-
"GET /simulate-transaction": {
|
|
190
|
-
accepts: { scheme: "exact", price: priceUsdc, network: `eip155:${CHAIN_ID}`, payTo: receiverAddress },
|
|
191
|
-
description: `Simulate EVM execution trace before broadcasting (USDC ${priceUsdc})`,
|
|
192
|
-
mimeType: "application/json",
|
|
193
|
-
},
|
|
194
|
-
}, resourceServer));
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
app.use(["/assess-risk", "/check-token-security", "/simulate-transaction"], paidConfigError);
|
|
198
|
-
}
|
|
199
|
-
app.get("/assess-risk", async (req, res) => {
|
|
200
|
-
try {
|
|
201
|
-
const { action, tokenIn, tokenOut, amount, toAddress, walletAddress } = req.query;
|
|
202
|
-
const result = await handleAssessRisk({
|
|
203
|
-
action: action,
|
|
204
|
-
tokenIn: tokenIn,
|
|
205
|
-
tokenOut: tokenOut,
|
|
206
|
-
amount: amount,
|
|
207
|
-
toAddress: toAddress,
|
|
208
|
-
walletAddress: walletAddress,
|
|
209
|
-
});
|
|
210
|
-
res.json(result);
|
|
211
|
-
}
|
|
212
|
-
catch (e) {
|
|
213
|
-
res.status(400).json(fail("BAD_REQUEST", e.message, false, "x402_server"));
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
app.get("/check-token-security", async (req, res) => {
|
|
217
|
-
try {
|
|
218
|
-
const { tokenAddress, chainId } = req.query;
|
|
219
|
-
const cid = chainId ? parseInt(chainId, 10) : CHAIN_ID;
|
|
220
|
-
const result = await handleCheckTokenSecurity({ tokenAddress: tokenAddress, chainId: cid });
|
|
221
|
-
res.json(result);
|
|
222
|
-
}
|
|
223
|
-
catch (e) {
|
|
224
|
-
res.status(400).json(fail("BAD_REQUEST", e.message, false, "x402_server"));
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
app.get("/simulate-transaction", async (req, res) => {
|
|
228
|
-
try {
|
|
229
|
-
const { action, tokenIn, tokenOut, amount, toAddress, walletAddress } = req.query;
|
|
230
|
-
const result = await handleSimulateTransaction({
|
|
231
|
-
action: action,
|
|
232
|
-
tokenIn: tokenIn,
|
|
233
|
-
tokenOut: tokenOut,
|
|
234
|
-
amount: amount,
|
|
235
|
-
toAddress: toAddress,
|
|
236
|
-
walletAddress: walletAddress,
|
|
237
|
-
});
|
|
238
|
-
res.json(result);
|
|
239
|
-
}
|
|
240
|
-
catch (e) {
|
|
241
|
-
res.status(400).json(fail("BAD_REQUEST", e.message, false, "x402_server"));
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
app.listen(port, () => {
|
|
245
|
-
console.log(`✅ SafeHands x402 Server listening on http://localhost:${port}`);
|
|
246
|
-
console.log(`📡 Network: eip155:${CHAIN_ID} (Pharos ${PHAROS_ENVIRONMENT})`);
|
|
247
|
-
console.log(`💰 Paid endpoints configured: ${isPaidConfigured ? "yes" : "no"}`);
|
|
248
|
-
if (receiverAddress)
|
|
249
|
-
console.log(`💰 Paid recipient address: ${receiverAddress}`);
|
|
250
|
-
console.log(`🪙 x402 payment token: ${usdcAddress}`);
|
|
251
|
-
console.log(`🆓 Free APIs: /supported, /health`);
|
|
252
|
-
});
|
|
1
|
+
// ─── SafeHands x402 Server ──────────────────────────────────────────────
|
|
2
|
+
// Exposes SafeHands risk endpoints. /supported and /health are free and do
|
|
3
|
+
// not require user private keys. Paid routes are gated only when receiver +
|
|
4
|
+
// facilitator signer config is present.
|
|
5
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
6
|
+
import express from "express";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { defineChain, createWalletClient, http, publicActions } from "viem";
|
|
10
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
11
|
+
import { x402Facilitator } from "@x402/core/facilitator";
|
|
12
|
+
import { toFacilitatorEvmSigner } from "@x402/evm";
|
|
13
|
+
import { ExactEvmScheme as FacilitatorExactEvmScheme } from "@x402/evm/exact/facilitator";
|
|
14
|
+
import { ExactEvmScheme as ServerExactEvmScheme } from "@x402/evm/exact/server";
|
|
15
|
+
import { HTTPFacilitatorClient } from "@x402/core/server";
|
|
16
|
+
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
|
|
17
|
+
import { handleAssessRisk } from "./tools/assessRisk.js";
|
|
18
|
+
import { handleCheckTokenSecurity } from "./tools/checkTokenSecurity.js";
|
|
19
|
+
import { handleSimulateTransaction } from "./tools/simulateTransaction.js";
|
|
20
|
+
import { CHAIN_ID, PHAROS_ENVIRONMENT, RPC_URL, X402_PAYMENT_TOKEN_ADDRESS } from "./lib/constants.js";
|
|
21
|
+
import { fail, ok } from "./lib/toolResponse.js";
|
|
22
|
+
function loadEnv() {
|
|
23
|
+
try {
|
|
24
|
+
const f = readFileSync(join(process.cwd(), ".env"), "utf-8");
|
|
25
|
+
for (const l of f.split("\n")) {
|
|
26
|
+
const t = l.trim();
|
|
27
|
+
if (!t || t.startsWith("#"))
|
|
28
|
+
continue;
|
|
29
|
+
const i = t.indexOf("=");
|
|
30
|
+
if (i === -1)
|
|
31
|
+
continue;
|
|
32
|
+
const key = t.slice(0, i).trim();
|
|
33
|
+
if (process.env[key] === undefined)
|
|
34
|
+
process.env[key] = t.slice(i + 1).trim();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
}
|
|
39
|
+
loadEnv();
|
|
40
|
+
const receiverAddress = (process.env.X402_PAY_TO || process.env.WALLET_ADDRESS || "");
|
|
41
|
+
const facilitatorPrivateKey = process.env.X402_FACILITATOR_PRIVATE_KEY || "";
|
|
42
|
+
const port = parseInt(process.env.X402_SERVER_PORT || process.env.X402_PORT || "4021", 10);
|
|
43
|
+
const rpcUrl = process.env.PHAROS_RPC_URL || RPC_URL;
|
|
44
|
+
const usdcAddress = X402_PAYMENT_TOKEN_ADDRESS;
|
|
45
|
+
const priceUsdc = process.env.X402_PRICE_USDC || "0.001";
|
|
46
|
+
const isPaidConfigured = Boolean(receiverAddress && facilitatorPrivateKey);
|
|
47
|
+
const pharos = defineChain({
|
|
48
|
+
id: CHAIN_ID,
|
|
49
|
+
name: `Pharos ${PHAROS_ENVIRONMENT}`,
|
|
50
|
+
nativeCurrency: { name: "PHRS", symbol: "PHRS", decimals: 18 },
|
|
51
|
+
rpcUrls: { default: { http: [rpcUrl] } },
|
|
52
|
+
testnet: true,
|
|
53
|
+
});
|
|
54
|
+
const app = express();
|
|
55
|
+
app.disable("x-powered-by");
|
|
56
|
+
app.use((req, res, next) => {
|
|
57
|
+
req.setTimeout(15_000);
|
|
58
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
59
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
60
|
+
next();
|
|
61
|
+
});
|
|
62
|
+
app.use(express.json({ limit: "64kb" }));
|
|
63
|
+
const rateLimitWindowMs = 60_000;
|
|
64
|
+
const rateLimitMax = 120;
|
|
65
|
+
const rateLimitBuckets = new Map();
|
|
66
|
+
app.use((req, res, next) => {
|
|
67
|
+
const key = req.ip || "unknown";
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const bucket = rateLimitBuckets.get(key);
|
|
70
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
71
|
+
rateLimitBuckets.set(key, { count: 1, resetAt: now + rateLimitWindowMs });
|
|
72
|
+
return next();
|
|
73
|
+
}
|
|
74
|
+
bucket.count += 1;
|
|
75
|
+
if (bucket.count > rateLimitMax) {
|
|
76
|
+
return res.status(429).json(fail("RATE_LIMITED", "Too many requests", true, "x402_server"));
|
|
77
|
+
}
|
|
78
|
+
return next();
|
|
79
|
+
});
|
|
80
|
+
let facilitator = null;
|
|
81
|
+
let resourceServer = null;
|
|
82
|
+
if (isPaidConfigured) {
|
|
83
|
+
const account = privateKeyToAccount(facilitatorPrivateKey);
|
|
84
|
+
const walletClient = createWalletClient({
|
|
85
|
+
account,
|
|
86
|
+
chain: pharos,
|
|
87
|
+
transport: http(undefined, { timeout: 30_000 }),
|
|
88
|
+
}).extend(publicActions);
|
|
89
|
+
const evmSigner = toFacilitatorEvmSigner({
|
|
90
|
+
address: account.address,
|
|
91
|
+
getCode: (args) => walletClient.getCode(args),
|
|
92
|
+
readContract: (args) => walletClient.readContract({ ...args, args: args.args || [] }),
|
|
93
|
+
verifyTypedData: (args) => walletClient.verifyTypedData(args),
|
|
94
|
+
writeContract: (args) => walletClient.writeContract({ ...args, args: args.args || [] }),
|
|
95
|
+
sendTransaction: (args) => walletClient.sendTransaction(args),
|
|
96
|
+
waitForTransactionReceipt: (args) => walletClient.waitForTransactionReceipt(args),
|
|
97
|
+
});
|
|
98
|
+
facilitator = new x402Facilitator();
|
|
99
|
+
facilitator.register(`eip155:${CHAIN_ID}`, new FacilitatorExactEvmScheme(evmSigner, {}));
|
|
100
|
+
const facilitatorClient = new HTTPFacilitatorClient({ url: `http://localhost:${port}` });
|
|
101
|
+
resourceServer = new x402ResourceServer(facilitatorClient);
|
|
102
|
+
const evmScheme = new ServerExactEvmScheme();
|
|
103
|
+
evmScheme.registerMoneyParser(async (amount, network) => {
|
|
104
|
+
if (network === `eip155:${CHAIN_ID}`) {
|
|
105
|
+
return {
|
|
106
|
+
amount: Math.round(amount * 1e6).toString(),
|
|
107
|
+
asset: usdcAddress,
|
|
108
|
+
extra: { token: "USDC", name: "USDC", version: "2" },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
});
|
|
113
|
+
resourceServer.register(`eip155:${CHAIN_ID}`, evmScheme);
|
|
114
|
+
}
|
|
115
|
+
app.post("/verify", async (req, res) => {
|
|
116
|
+
if (!facilitator) {
|
|
117
|
+
return res.status(503).json(fail("X402_SERVER_RECEIVER_CONFIG_MISSING", "x402 facilitator is not configured. Set X402_PAY_TO/WALLET_ADDRESS and X402_FACILITATOR_PRIVATE_KEY for paid endpoints. Client PRIVATE_KEY is not used for server receiver config.", false, "x402_server"));
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const { paymentPayload, paymentRequirements } = req.body;
|
|
121
|
+
const result = await facilitator.verify(paymentPayload, paymentRequirements);
|
|
122
|
+
res.json(result);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
res.status(500).json(fail("X402_PAYMENT_FAILED", e.message, true, "x402_server"));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
app.post("/settle", async (req, res) => {
|
|
129
|
+
if (!facilitator) {
|
|
130
|
+
return res.status(503).json(fail("X402_SERVER_RECEIVER_CONFIG_MISSING", "x402 facilitator is not configured. Set X402_PAY_TO/WALLET_ADDRESS and X402_FACILITATOR_PRIVATE_KEY for paid endpoints. Client PRIVATE_KEY is not used for server receiver config.", false, "x402_server"));
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const { paymentPayload, paymentRequirements } = req.body;
|
|
134
|
+
const result = await facilitator.settle(paymentPayload, paymentRequirements);
|
|
135
|
+
res.json(result);
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
res.status(500).json(fail("X402_PAYMENT_FAILED", e.message, true, "x402_server"));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
app.get("/supported", (_req, res) => {
|
|
142
|
+
if (facilitator)
|
|
143
|
+
return res.json(facilitator.getSupported());
|
|
144
|
+
return res.json(ok({
|
|
145
|
+
configured: false,
|
|
146
|
+
network: `eip155:${CHAIN_ID}`,
|
|
147
|
+
scheme: "exact",
|
|
148
|
+
asset: usdcAddress,
|
|
149
|
+
priceUsdc,
|
|
150
|
+
freeEndpoints: ["GET /supported", "GET /health"],
|
|
151
|
+
paidEndpoints: ["GET /assess-risk", "GET /check-token-security", "GET /simulate-transaction"],
|
|
152
|
+
configError: {
|
|
153
|
+
code: "X402_SERVER_RECEIVER_CONFIG_MISSING",
|
|
154
|
+
message: "Paid endpoints require receiver/payTo plus facilitator signer config. Free endpoints remain available.",
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
});
|
|
158
|
+
app.get("/health", (_req, res) => {
|
|
159
|
+
res.json(ok({
|
|
160
|
+
status: "ok",
|
|
161
|
+
service: "SafeHands x402 Resource Server",
|
|
162
|
+
network: `Pharos ${PHAROS_ENVIRONMENT} (Chain ID ${CHAIN_ID})`,
|
|
163
|
+
environment: PHAROS_ENVIRONMENT,
|
|
164
|
+
isMainnet: false,
|
|
165
|
+
receiverConfigured: Boolean(receiverAddress),
|
|
166
|
+
paidEndpointsConfigured: isPaidConfigured,
|
|
167
|
+
receiver: receiverAddress || null,
|
|
168
|
+
asset: usdcAddress,
|
|
169
|
+
freeEndpoints: ["/supported", "/health"],
|
|
170
|
+
}));
|
|
171
|
+
});
|
|
172
|
+
function paidConfigError(req, res, next) {
|
|
173
|
+
if (isPaidConfigured)
|
|
174
|
+
return next();
|
|
175
|
+
return res.status(503).json(fail("X402_SERVER_RECEIVER_CONFIG_MISSING", "Paid endpoint receiver/payTo or facilitator signer config is missing. Free endpoints do not require a client signer/private key.", false, "x402_server"));
|
|
176
|
+
}
|
|
177
|
+
if (isPaidConfigured && resourceServer) {
|
|
178
|
+
app.use(paymentMiddleware({
|
|
179
|
+
"GET /assess-risk": {
|
|
180
|
+
accepts: { scheme: "exact", price: priceUsdc, network: `eip155:${CHAIN_ID}`, payTo: receiverAddress },
|
|
181
|
+
description: `Assess transaction risk (USDC ${priceUsdc})`,
|
|
182
|
+
mimeType: "application/json",
|
|
183
|
+
},
|
|
184
|
+
"GET /check-token-security": {
|
|
185
|
+
accepts: { scheme: "exact", price: priceUsdc, network: `eip155:${CHAIN_ID}`, payTo: receiverAddress },
|
|
186
|
+
description: `Verify contract token security profile (USDC ${priceUsdc})`,
|
|
187
|
+
mimeType: "application/json",
|
|
188
|
+
},
|
|
189
|
+
"GET /simulate-transaction": {
|
|
190
|
+
accepts: { scheme: "exact", price: priceUsdc, network: `eip155:${CHAIN_ID}`, payTo: receiverAddress },
|
|
191
|
+
description: `Simulate EVM execution trace before broadcasting (USDC ${priceUsdc})`,
|
|
192
|
+
mimeType: "application/json",
|
|
193
|
+
},
|
|
194
|
+
}, resourceServer));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
app.use(["/assess-risk", "/check-token-security", "/simulate-transaction"], paidConfigError);
|
|
198
|
+
}
|
|
199
|
+
app.get("/assess-risk", async (req, res) => {
|
|
200
|
+
try {
|
|
201
|
+
const { action, tokenIn, tokenOut, amount, toAddress, walletAddress } = req.query;
|
|
202
|
+
const result = await handleAssessRisk({
|
|
203
|
+
action: action,
|
|
204
|
+
tokenIn: tokenIn,
|
|
205
|
+
tokenOut: tokenOut,
|
|
206
|
+
amount: amount,
|
|
207
|
+
toAddress: toAddress,
|
|
208
|
+
walletAddress: walletAddress,
|
|
209
|
+
});
|
|
210
|
+
res.json(result);
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
res.status(400).json(fail("BAD_REQUEST", e.message, false, "x402_server"));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
app.get("/check-token-security", async (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
const { tokenAddress, chainId } = req.query;
|
|
219
|
+
const cid = chainId ? parseInt(chainId, 10) : CHAIN_ID;
|
|
220
|
+
const result = await handleCheckTokenSecurity({ tokenAddress: tokenAddress, chainId: cid });
|
|
221
|
+
res.json(result);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
res.status(400).json(fail("BAD_REQUEST", e.message, false, "x402_server"));
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
app.get("/simulate-transaction", async (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
const { action, tokenIn, tokenOut, amount, toAddress, walletAddress } = req.query;
|
|
230
|
+
const result = await handleSimulateTransaction({
|
|
231
|
+
action: action,
|
|
232
|
+
tokenIn: tokenIn,
|
|
233
|
+
tokenOut: tokenOut,
|
|
234
|
+
amount: amount,
|
|
235
|
+
toAddress: toAddress,
|
|
236
|
+
walletAddress: walletAddress,
|
|
237
|
+
});
|
|
238
|
+
res.json(result);
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
res.status(400).json(fail("BAD_REQUEST", e.message, false, "x402_server"));
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
app.listen(port, () => {
|
|
245
|
+
console.log(`✅ SafeHands x402 Server listening on http://localhost:${port}`);
|
|
246
|
+
console.log(`📡 Network: eip155:${CHAIN_ID} (Pharos ${PHAROS_ENVIRONMENT})`);
|
|
247
|
+
console.log(`💰 Paid endpoints configured: ${isPaidConfigured ? "yes" : "no"}`);
|
|
248
|
+
if (receiverAddress)
|
|
249
|
+
console.log(`💰 Paid recipient address: ${receiverAddress}`);
|
|
250
|
+
console.log(`🪙 x402 payment token: ${usdcAddress}`);
|
|
251
|
+
console.log(`🆓 Free APIs: /supported, /health`);
|
|
252
|
+
});
|
|
253
253
|
//# sourceMappingURL=x402Server.js.map
|