Bot & Automation
Market AI
/root/hermes-projects/Market AI
packages/provider-adapters/src/trade-executor.ts
text
import { SOL_MINT, logger, type TradeExecutionMode, type TradeExecutionResult, type TradeQuote, type TradeRequest } from "@market-ai/core";
import type { TradeExecutor } from "./provider-types.js";
function baseWarnings(request: TradeRequest): string[] {
return [
"Meme coin bisa rug-pull dan liquidity bisa hilang dalam hitungan menit.",
`Slippage diset ${request.slippageBps / 100}%; transaksi bisa gagal atau dapat harga jauh lebih buruk.`,
request.mode === "live" ? "Live trade akan memakai executor eksternal jika dikonfigurasi." : "Paper trade tidak mengirim transaksi on-chain.",
];
}
function getExecutionMode(): TradeExecutionMode {
return process.env.TRADING_MODE === "live" ? "live" : "paper";
}
export const tradeExecutor: TradeExecutor = {
id: "guarded-trade-executor",
displayName: "Guarded Trade Executor",
async buildQuote(request: TradeRequest): Promise<TradeQuote> {
const id = `TRD-${crypto.randomUUID().slice(0, 8).toUpperCase()}`;
const mode: TradeExecutionMode = getExecutionMode() === "live" && request.mode === "live" ? "live" : "paper";
const normalizedRequest: TradeRequest = { ...request, mode };
if (mode === "live" && process.env.JUPITER_API_BASE_URL && request.chainId === "solana" && request.side === "BUY") {
try {
const quoteUrl = new URL("/swap/v1/quote", process.env.JUPITER_API_BASE_URL);
quoteUrl.searchParams.set("inputMint", SOL_MINT);
quoteUrl.searchParams.set("outputMint", request.tokenAddress);
quoteUrl.searchParams.set("amount", String(Math.round(request.amount * 1_000_000_000)));
quoteUrl.searchParams.set("slippageBps", String(request.slippageBps));
const response = await fetch(quoteUrl, {
headers: process.env.JUPITER_API_KEY ? { "x-api-key": process.env.JUPITER_API_KEY } : undefined,
});
if (response.ok) {
const body = (await response.json()) as { outAmount?: string; priceImpactPct?: string; routePlan?: Array<{ swapInfo?: { label?: string } }> };
return {
id,
request: normalizedRequest,
inputSymbol: "SOL",
outputSymbol: "TOKEN",
estimatedOutput: body.outAmount ?? "0",
priceImpactPct: body.priceImpactPct ? Number(body.priceImpactPct) : undefined,
routeSummary: body.routePlan?.map((route) => route.swapInfo?.label).filter(Boolean).join(" -> ") || "Jupiter route",
expiresAt: new Date(Date.now() + 30_000).toISOString(),
warnings: baseWarnings(normalizedRequest),
};
}
logger.error("jupiter_quote_failed", { status: response.status });
} catch (error) {
logger.error("jupiter_quote_error", { message: error instanceof Error ? error.message : "unknown" });
}
}
return {
id,
request: normalizedRequest,
inputSymbol: request.side === "BUY" ? "SOL" : "TOKEN",
outputSymbol: request.side === "BUY" ? "TOKEN" : "SOL",
estimatedOutput: mode === "paper" ? "paper-estimate" : "executor-estimate",
routeSummary: mode === "paper" ? "paper-trading-simulation" : "external-live-executor",
expiresAt: new Date(Date.now() + 60_000).toISOString(),
warnings: baseWarnings(normalizedRequest),
};
},
async executeTrade(quote: TradeQuote): Promise<TradeExecutionResult> {
if (quote.request.mode === "paper") {
return {
id: quote.id,
mode: "paper",
status: "filled",
quote,
message: "Paper trade recorded. Tidak ada transaksi on-chain yang dikirim.",
executedAt: new Date().toISOString(),
};
}
if (!process.env.TRADE_EXECUTOR_URL || !process.env.TRADE_EXECUTOR_TOKEN) {
return {
id: quote.id,
mode: "live",
status: "rejected",
quote,
message: "Live trade ditolak karena TRADE_EXECUTOR_URL dan TRADE_EXECUTOR_TOKEN belum dikonfigurasi.",
executedAt: new Date().toISOString(),
};
}
const response = await fetch(process.env.TRADE_EXECUTOR_URL, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${process.env.TRADE_EXECUTOR_TOKEN}`,
},
body: JSON.stringify({ quote }),
});
if (!response.ok) {
return {
id: quote.id,
mode: "live",
status: "failed",
quote,
message: `Live executor gagal dengan status ${response.status}.`,
executedAt: new Date().toISOString(),
};
}
const body = (await response.json()) as { txHash?: string; status?: "submitted" | "filled"; message?: string };
return {
id: quote.id,
mode: "live",
status: body.status ?? "submitted",
txHash: body.txHash,
quote,
message: body.message ?? "Live trade submitted through external executor.",
executedAt: new Date().toISOString(),
};
},
};