majorly improve SOL push UI + allow for more customization.

This commit is contained in:
KingKDot
2026-07-14 00:03:55 -05:00
parent 0a645d3280
commit 4042d4f768
12 changed files with 506 additions and 168 deletions
+11
View File
@@ -3,6 +3,7 @@ import { getConfig } from "./config";
import {
clearRequestRateLimitsForTests,
consumeLoginPageRateLimit,
consumeSolRpcRateLimit,
consumeUnauthorizedRateLimit,
isRateLimited,
recordFailedAttempt,
@@ -59,4 +60,14 @@ describe("rateLimit", () => {
expect(result.limited).toBe(true);
expect(result.retryAfter).toBeGreaterThan(0);
});
test("applies separate Solana publish and balance limits per user", () => {
const userId = Date.now();
for (let i = 0; i < 3; i += 1) {
expect(consumeSolRpcRateLimit(userId, "publish").limited).toBe(false);
}
expect(consumeSolRpcRateLimit(userId, "publish").limited).toBe(true);
expect(consumeSolRpcRateLimit(userId, "balance").limited).toBe(false);
expect(consumeSolRpcRateLimit(userId + 1, "publish").limited).toBe(false);
});
});
+10
View File
@@ -162,6 +162,16 @@ export function consumeUnauthorizedRateLimit(ip: string): { limited: boolean; re
return consumeRequestRateLimit(`unauthorized:${ip}`, "Unauthorized request", policy);
}
export function consumeSolRpcRateLimit(
userId: number,
action: "balance" | "publish",
): { limited: boolean; retryAfter?: number } {
const policy = action === "publish"
? makeRequestPolicy("SOL_PUBLISH", { maxRequests: 3, windowMs: 5 * 60 * 1000, lockoutMs: 5 * 60 * 1000 })
: makeRequestPolicy("SOL_BALANCE", { maxRequests: 12, windowMs: 60 * 1000, lockoutMs: 60 * 1000 });
return consumeRequestRateLimit(`sol-${action}:${userId}`, `Solana ${action}`, policy);
}
export function clearRequestRateLimitsForTests(): void {
requestRateLimitStore.clear();
}
@@ -29,6 +29,7 @@ import { resolveRuntimeRoot } from "../runtime-paths";
import { createUploadPull } from "../file-transfer-state";
import { handleBuildProfileRoutes } from "./build-profile-routes";
import { sanitizeInitialClientTag } from "../build-config-sanitize";
import { getSolRpcEndpointUrls, normalizeSolRpcUrls } from "../../sol-rpc-endpoints";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
@@ -287,8 +288,22 @@ export async function handleBuildRoutes(
}
safeSolAddress = trimmedAddr;
if (typeof solRpcEndpoints === "string" && solRpcEndpoints.trim()) {
const endpoints = solRpcEndpoints.split("\n").map((e: string) => e.trim()).filter(Boolean);
const requestedEndpoints = typeof solRpcEndpoints === "string" && solRpcEndpoints.trim()
? solRpcEndpoints.split("\n").map((e: string) => e.trim()).filter(Boolean)
: getSolRpcEndpointUrls();
if (requestedEndpoints.length === 0) {
return Response.json(
{ error: "Solana memo mode requires at least one RPC endpoint" },
{ status: 400 },
);
}
if (requestedEndpoints.length > 0) {
let endpoints: string[];
try {
endpoints = normalizeSolRpcUrls(requestedEndpoints);
} catch (error: any) {
return Response.json({ error: error?.message || "Invalid RPC endpoints" }, { status: 400 });
}
for (const ep of endpoints) {
try {
const parsed = new URL(ep);
+64 -33
View File
@@ -3,6 +3,12 @@ import { authenticateRequest } from "../../auth";
import { getConfig } from "../../config";
import { logger } from "../../logger";
import { requirePermission } from "../../rbac";
import { consumeSolRpcRateLimit } from "../../rateLimit";
import {
getSolRpcEndpoints,
normalizeSolRpcUrl,
saveSolRpcEndpointUrls,
} from "../../sol-rpc-endpoints";
let _solana: typeof import("@solana/web3.js") | null = null;
async function getSolana() {
@@ -36,19 +42,6 @@ function decodeBase58(str: string): Uint8Array {
return new Uint8Array(bytes.reverse());
}
const DEFAULT_RPC_ENDPOINTS = [
"https://api.mainnet-beta.solana.com",
"https://solana-mainnet.gateway.tatum.io",
"https://go.getblock.us/86aac42ad4484f3c813079afc201451c",
"https://solana-rpc.publicnode.com",
"https://api.blockeden.xyz/solana/KeCh6p22EX5AeRHxMSmc",
"https://solana.drpc.org",
"https://solana.leorpc.com/?api_key=FREE",
"https://solana.api.onfinality.io/public",
"https://solana.api.pocket.network/",
"https://api.devnet.solana.com",
];
function encryptServerUrl(serverUrl: string, agentToken: string): string {
const keyHash = createHash("sha256").update(agentToken).digest();
const nonce = randomBytes(12);
@@ -72,11 +65,49 @@ export async function handleSolRoutes(
return new Response("Unauthorized", { status: 401 });
}
try {
requirePermission(user, "system:configure");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
if (req.method === "GET" && url.pathname === "/api/sol/rpc-endpoints") {
const records = getSolRpcEndpoints();
return Response.json({ endpoints: records.map((item) => item.url), records });
}
try { requirePermission(user, "system:configure"); }
catch (error) { return error instanceof Response ? error : new Response("Forbidden", { status: 403 }); }
if (req.method === "POST" && url.pathname === "/api/sol/rpc-endpoints") {
const body = await req.json();
const current = getSolRpcEndpoints().map((item) => item.url);
try {
const endpoint = normalizeSolRpcUrl(body?.url);
if (current.includes(endpoint)) {
return Response.json({ error: "RPC endpoint is already in the list" }, { status: 409 });
}
const records = saveSolRpcEndpointUrls([...current, endpoint], user.userId);
return Response.json({ records, endpoints: records.map((item) => item.url) }, { status: 201 });
} catch (error: any) {
return Response.json({ error: error?.message || "Invalid RPC endpoint" }, { status: 400 });
}
}
const endpointMatch = url.pathname.match(/^\/api\/sol\/rpc-endpoints\/([a-f0-9]{16})$/);
if (endpointMatch && (req.method === "PATCH" || req.method === "DELETE")) {
const current = getSolRpcEndpoints();
const index = current.findIndex((item) => item.id === endpointMatch[1]);
if (index < 0) return Response.json({ error: "RPC endpoint not found" }, { status: 404 });
const urls = current.map((item) => item.url);
if (req.method === "DELETE") urls.splice(index, 1);
else {
try {
const endpoint = normalizeSolRpcUrl((await req.json())?.url);
if (urls.some((item, itemIndex) => itemIndex !== index && item === endpoint)) {
return Response.json({ error: "RPC endpoint is already in the list" }, { status: 409 });
}
urls[index] = endpoint;
} catch (error: any) {
return Response.json({ error: error?.message || "Invalid RPC endpoint" }, { status: 400 });
}
}
const records = saveSolRpcEndpointUrls(urls, user.userId);
return Response.json({ records, endpoints: records.map((item) => item.url) });
}
if (req.method === "POST" && url.pathname === "/api/sol/preview") {
@@ -98,6 +129,11 @@ export async function handleSolRoutes(
}
if (req.method === "POST" && url.pathname === "/api/sol/publish") {
const rateLimit = consumeSolRpcRateLimit(user.userId, "publish");
if (rateLimit.limited) return Response.json(
{ error: "Solana publish rate limit exceeded" },
{ status: 429, headers: { "Retry-After": String(rateLimit.retryAfter || 60) } },
);
const body = await req.json();
const { privateKeyBase58, serverUrl, rpcUrl } = body;
@@ -123,14 +159,11 @@ export async function handleSolRoutes(
return Response.json({ error: "Invalid Solana private key (Base58)" }, { status: 400 });
}
const endpoint = typeof rpcUrl === "string" && rpcUrl.trim()
? rpcUrl.trim()
: "https://api.mainnet-beta.solana.com";
let endpoint: string;
try {
new URL(endpoint);
} catch {
return Response.json({ error: "Invalid RPC URL" }, { status: 400 });
endpoint = normalizeSolRpcUrl(rpcUrl);
} catch (err: any) {
return Response.json({ error: err?.message || "Invalid RPC URL" }, { status: 400 });
}
const memo = encryptServerUrl(serverUrl.trim(), agentToken);
@@ -182,6 +215,11 @@ export async function handleSolRoutes(
}
if (req.method === "POST" && url.pathname === "/api/sol/balance") {
const rateLimit = consumeSolRpcRateLimit(user.userId, "balance");
if (rateLimit.limited) return Response.json(
{ error: "Solana balance rate limit exceeded" },
{ status: 429, headers: { "Retry-After": String(rateLimit.retryAfter || 60) } },
);
const body = await req.json();
const { publicKeyBase58, rpcUrl } = body;
@@ -197,11 +235,8 @@ export async function handleSolRoutes(
return Response.json({ error: "Invalid Solana public key" }, { status: 400 });
}
const endpoint = typeof rpcUrl === "string" && rpcUrl.trim()
? rpcUrl.trim()
: "https://api.mainnet-beta.solana.com";
try {
const endpoint = normalizeSolRpcUrl(rpcUrl);
const { Connection, LAMPORTS_PER_SOL } = await getSolana();
const connection = new Connection(endpoint, "confirmed");
const balance = await connection.getBalance(pubkey);
@@ -216,10 +251,6 @@ export async function handleSolRoutes(
}
}
if (req.method === "GET" && url.pathname === "/api/sol/rpc-endpoints") {
return Response.json({ endpoints: DEFAULT_RPC_ENDPOINTS });
}
return null;
} catch (err: any) {
logger.error(`[sol] Route error: ${err?.message || err}`);
+79
View File
@@ -0,0 +1,79 @@
import { createHash } from "crypto";
import { getSharedUiSettings, saveSharedUiSettings } from "./db";
const SETTINGS_SCOPE = "sol-rpc-endpoints";
export const MAX_SOL_RPC_ENDPOINTS = 20;
export const DEFAULT_SOL_RPC_ENDPOINTS = [
"https://api.mainnet.solana.com/",
"https://solana-rpc.publicnode.com/",
"https://solana.api.pocket.network/",
"https://solana.leorpc.com/?api_key=FREE",
"https://public.rpc.solanavibestation.com/",
"https://api.uniblock.dev/uni/v1/json-rpc?chainId=solana",
];
export type SolRpcEndpoint = { id: string; url: string };
export function normalizeSolRpcUrl(value: unknown): string {
if (typeof value !== "string") throw new Error("RPC endpoint must be a URL string");
const trimmed = value.trim();
if (!trimmed) throw new Error("RPC endpoint is required");
if (trimmed.length > 2048) throw new Error("RPC endpoint is too long");
let parsed: URL;
try {
parsed = new URL(trimmed);
} catch {
throw new Error("Invalid RPC endpoint URL");
}
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
throw new Error("RPC endpoint must use HTTP or HTTPS");
}
if (parsed.username || parsed.password) {
throw new Error("RPC endpoint must not contain URL credentials");
}
return parsed.toString();
}
export function normalizeSolRpcUrls(values: unknown): string[] {
if (!Array.isArray(values)) throw new Error("endpoints must be an array");
const result: string[] = [];
const seen = new Set<string>();
for (const value of values) {
const normalized = normalizeSolRpcUrl(value);
if (!seen.has(normalized)) {
seen.add(normalized);
result.push(normalized);
}
}
if (result.length > MAX_SOL_RPC_ENDPOINTS) {
throw new Error(`A maximum of ${MAX_SOL_RPC_ENDPOINTS} RPC endpoints is allowed`);
}
return result;
}
function endpointId(url: string): string {
return createHash("sha256").update(url).digest("hex").slice(0, 16);
}
export function getSolRpcEndpointUrls(): string[] {
const record = getSharedUiSettings(SETTINGS_SCOPE);
if (!record) return [...DEFAULT_SOL_RPC_ENDPOINTS];
try {
const parsed = JSON.parse(record.settingsJson);
return normalizeSolRpcUrls(parsed?.endpoints);
} catch {
return [...DEFAULT_SOL_RPC_ENDPOINTS];
}
}
export function getSolRpcEndpoints(): SolRpcEndpoint[] {
return getSolRpcEndpointUrls().map((url) => ({ id: endpointId(url), url }));
}
export function saveSolRpcEndpointUrls(values: unknown, userId: number): SolRpcEndpoint[] {
const endpoints = normalizeSolRpcUrls(values);
saveSharedUiSettings(SETTINGS_SCOPE, JSON.stringify({ endpoints }), userId);
return endpoints.map((url) => ({ id: endpointId(url), url }));
}