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(),
    };
  },
};