Files
gmgn-skills/src/client/OpenApiClient.ts
T
David Lau 2d7d0043e6 feat(chain): add robinhood support; disable monad/megaeth/hyperevm/tron
Add robinhood as a supported chain across the CLI and skills, and stop
advertising chains that are not publicly supported yet.

robinhood:
- validate.ts: add to VALID_CHAINS and treat as EVM for address checks
- reject guards for commands that do not support it: track kol,
  track smartmoney, cooking create (mirrors market signal)
- --chain help updated for supported commands (token/portfolio/swap/
  market kline·trending·trenches·hot-searches/track follow-*/order get/
  gas-price); OpenApiClient chain comment
- docs: all SKILL.md, Readme.md, Readme.zh.md (split-out row for
  unsupported commands), cli-usage.md, plugin/marketplace descriptions

disable monad/megaeth/hyperevm/tron:
- comment monad out of VALID_CHAINS (kept for quick re-enable)
- strip these chains from all --chain help, SKILL.md, cli-usage.md
  (hot-searches default now 4 chains: sol/bsc/base/eth)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:48:54 +08:00

893 lines
28 KiB
TypeScript

/**
* OpenApiClient — GMGN OpenAPI external client
*
* Auth modes:
* Exist (market/token/portfolio): X-APIKEY + timestamp + client_id
* Signed (swap and order routes): X-APIKEY + timestamp + client_id + X-Signature (private key signature)
*/
import { createRequire } from "node:module";
import { buildAuthQuery, buildMessage, detectAlgorithm, sign } from "./signer.js";
const RATE_LIMIT_RETRY_BUFFER_MS = 1000;
const DEFAULT_RATE_LIMIT_AUTO_RETRY_MAX_WAIT_MS = 5000;
const { version: CLI_VERSION } = createRequire(import.meta.url)("../../package.json") as { version: string };
const USER_AGENT = `gmgn-cli/${CLI_VERSION}`;
interface PreparedRequest {
method: string;
subPath: string;
url: string;
headers: Record<string, string>;
body: string | null;
curlStr: string;
}
interface ResponseEnvelope {
code: number | string;
data?: unknown;
message?: string;
error?: string;
}
interface OpenApiErrorParams {
method: string;
path: string;
status: number;
apiCode?: number | string;
apiError?: string;
apiMessage?: string;
resetAtUnix?: number;
}
class OpenApiError extends Error {
readonly method: string;
readonly path: string;
readonly status: number;
readonly apiCode?: number | string;
readonly apiError?: string;
readonly apiMessage?: string;
readonly resetAtUnix?: number;
constructor(params: OpenApiErrorParams) {
super(buildOpenApiErrorMessage(params));
this.name = "OpenApiError";
this.method = params.method;
this.path = params.path;
this.status = params.status;
this.apiCode = params.apiCode;
this.apiError = params.apiError;
this.apiMessage = params.apiMessage;
this.resetAtUnix = params.resetAtUnix;
}
}
export interface Config {
apiKey: string;
privateKeyPem?: string;
host: string;
}
export interface SwapParams {
chain: string;
from_address: string;
input_token: string;
output_token: string;
input_amount: string;
swap_mode?: string;
input_amount_bps?: string;
output_amount?: string;
slippage?: number;
auto_slippage?: boolean;
min_output_amount?: string;
is_anti_mev?: boolean;
priority_fee?: string;
tip_fee?: string;
gas_price?: string;
gas_level?: string;
auto_fee?: boolean;
max_fee_per_gas?: string;
max_priority_fee_per_gas?: string;
condition_orders?: StrategyConditionOrder[];
sell_ratio_type?: string;
}
export interface StrategyConditionOrder {
order_type: string; // "profit_stop" | "loss_stop" | "profit_stop_trace" | "loss_stop_trace"
side: string; // "sell"
price_scale?: string;
sell_ratio: string;
drawdown_rate?: string;
}
export interface MultiSwapParams {
chain: string;
accounts: string[];
input_token: string;
output_token: string;
input_amount?: Record<string, string>;
input_amount_bps?: Record<string, string>;
output_amount?: Record<string, string>;
swap_mode?: string;
slippage?: number;
auto_slippage?: boolean;
is_anti_mev?: boolean;
priority_fee?: string;
tip_fee?: string;
gas_price?: string;
gas_level?: string;
auto_fee?: boolean;
max_fee_per_gas?: string;
max_priority_fee_per_gas?: string;
condition_orders?: StrategyConditionOrder[];
sell_ratio_type?: string;
}
export interface StrategyCreateParams {
chain: string;
from_address: string;
base_token: string;
quote_token: string;
order_type: string;
sub_order_type: string;
check_price?: string;
open_price?: string;
amount_in?: string;
amount_in_percent?: string;
limit_price_mode?: string;
price_gap_ratio?: string;
expire_in?: number;
sell_ratio_type?: string;
slippage?: number;
auto_slippage?: boolean;
fee?: string;
auto_fee?: boolean;
gas_price?: string;
gas_level?: string;
max_fee_per_gas?: string;
max_priority_fee_per_gas?: string;
is_anti_mev?: boolean;
anti_mev_mode?: string;
priority_fee?: string;
tip_fee?: string;
custom_rpc?: string;
condition_orders?: StrategyConditionOrder[];
quote_investment?: string;
sell_param?: TradeParam;
buy_param?: TradeParam;
}
export interface StrategyCancelParams {
chain: string;
from_address: string;
order_id: string;
order_type?: string;
close_sell_model?: string;
}
export interface TokenSignalGroup {
signal_type?: number[];
mc_min?: number;
mc_max?: number;
trigger_mc_min?: number;
trigger_mc_max?: number;
total_fee_min?: number;
total_fee_max?: number;
min_create_or_open_ts?: string;
max_create_or_open_ts?: string;
}
// HotSearchesParam carries its filter fields flattened (no nested `filter` object):
// label/interval/chain plus optional `filters` boolean tags, `limit`, and rank-style
// numeric range bounds (min_<metric>/max_<metric>) incl. min_created/max_created.
// Extra range keys are forwarded verbatim; the service translates metric names per
// interval. See `market hot-searches` docs for the supported metric list.
export interface HotSearchesParam {
label?: string;
interval: string; // "1m" | "5m" | "1h" | "6h" | "24h"
chain: string; // "sol" | "bsc" | "base" | "eth" | "robinhood"
filters?: string[];
limit?: number;
[key: string]: string[] | number | string | undefined;
}
export interface PumpFeeShareInfo {
provider: string; // "solana" | "twitter" | "github"
username: string; // platform username; a SOL address when provider = "solana"
basic_points: number;
}
export interface BAGSFeeShareInfo {
provider: string; // "twitter" | "solana" | "kick" | "github"
username: string;
basic_points: number;
}
export interface FlapRateConf {
tax_rate?: number; // V5 unified tax rate, e.g. 5% -> 500
buy_tax_rate?: number; // V6 separate buy tax rate
sell_tax_rate?: number; // V6 separate sell tax rate
mkt_bps?: number;
deflation_bps?: number;
dividend_bps?: number;
lp_bps?: number;
minimum_share_balance?: number;
recipient_type?: string;
beneficiary?: string;
twitter_account?: string;
split_conf?: Array<{ recipient: string; bps: number }>;
}
export interface FourmemeRateConf {
fee_plan?: boolean;
fee_rate?: number;
burn_rate?: number;
divide_rate?: number;
liquidity_rate?: number;
recipient_rate?: number;
recipient_address?: string;
min_sharing?: number;
}
export interface BuyWalletInfo {
from_address: string;
buy_amt: string;
}
// Buy/sell execution config for CondMarket orders (snipe buy, auto-sell, pending_sell).
// Does NOT affect the main creation tx. Falls back to outer-level fields when omitted.
export interface TradeParam {
slippage?: number;
auto_slippage?: boolean;
fee?: string;
priority_fee?: string;
tip_fee?: string;
gas_price?: string;
max_priority_fee_per_gas?: string;
max_fee_per_gas?: string;
is_anti_mev?: boolean; // backend currently forces true; passing has no effect
anti_mev_mode?: string; // backend currently forces "secure"; passing has no effect
}
export interface CookingSellConfig {
sell_type: string; // "delay_sell" | "limit_order"
delay_sec?: number; // required when sell_type = "delay_sell"
delay_mili_sec?: number; // optional; takes precedence over delay_sec
sell_ratio: string; // "1" = 100%, "0.5" = 50%
check_price?: string; // trigger market cap in USD; required when sell_type = "limit_order"
wallet_addresses: string[]; // empty array means the strategy is inert
}
export interface CreateTokenParams {
// Required
chain: string;
dex: string;
from_address: string;
name: string;
symbol: string;
buy_amt: string;
// Image (one required)
image?: string;
image_url?: string;
// Social
description?: string;
website?: string;
twitter?: string;
telegram?: string;
// Transaction
slippage?: number;
auto_slippage?: boolean;
// Fees
fee?: string;
priority_fee?: string;
tip_fee?: string;
gas_price?: string;
max_priority_fee_per_gas?: string;
max_fee_per_gas?: string;
// Anti-MEV (SOL only)
is_anti_mev?: boolean;
anti_mev_mode?: string;
// Advanced
raised_token?: string;
dev_gas?: string;
dev_priority?: string;
dev_tip?: string;
dev_max_fee_per_gas?: string;
approve_vision?: string;
source?: string;
dev_wallet_bps?: number;
// CondMarket buy/sell execution config (snipe / bundle / auto-sell)
buy_trade_config?: TradeParam;
sell_trade_config?: TradeParam;
// Auto-sell strategies created after a successful launch
sell_configs?: CookingSellConfig[];
// Pump.fun specific
is_mayhem?: boolean;
is_cashback?: boolean;
is_buy_back?: boolean;
pump_fee_share_list?: PumpFeeShareInfo[];
// Flap specific
flap_rate_conf?: FlapRateConf;
// FourMeme specific
fourmeme_rate_conf?: FourmemeRateConf;
// BAGS specific
bags_fee_share_list?: BAGSFeeShareInfo[];
// Bonk specific
bonk_model?: string;
// Multi-wallet buy
buy_wallets?: BuyWalletInfo[];
snip_buy_wallets?: BuyWalletInfo[];
}
export class OpenApiClient {
private readonly apiKey: string;
private readonly privateKeyPem: string | undefined;
private readonly host: string;
constructor(config: Config) {
this.apiKey = config.apiKey;
this.privateKeyPem = config.privateKeyPem;
this.host = config.host.replace(/\/$/, "");
}
// ---- Token endpoints (exist auth) ----
async getTokenInfo(chain: string, address: string): Promise<unknown> {
return this.authExistRequest("GET", "/v1/token/info", { chain, address });
}
async getTokenSecurity(chain: string, address: string): Promise<unknown> {
return this.authExistRequest("GET", "/v1/token/security", { chain, address });
}
async getTokenPoolInfo(chain: string, address: string): Promise<unknown> {
return this.authExistRequest("GET", "/v1/token/pool_info", { chain, address });
}
async getTokenTopHolders(chain: string, address: string, extra: Record<string, string | number> = {}): Promise<unknown> {
return this.authExistRequest("GET", "/v1/market/token_top_holders", { chain, address, ...extra });
}
async getTokenTopTraders(chain: string, address: string, extra: Record<string, string | number> = {}): Promise<unknown> {
return this.authExistRequest("GET", "/v1/market/token_top_traders", { chain, address, ...extra });
}
// ---- Market endpoints (exist auth) ----
async getTokenKline(
chain: string,
address: string,
resolution: string,
from?: number,
to?: number
): Promise<unknown> {
const query: Record<string, string | number> = { chain, address, resolution };
if (from != null) query["from"] = from;
if (to != null) query["to"] = to;
return this.authExistRequest("GET", "/v1/market/token_kline", query);
}
// ---- Portfolio endpoints ----
async getWalletHoldings(
chain: string,
walletAddress: string,
extra: Record<string, string | number> = {}
): Promise<unknown> {
return this.authSignedRequest("GET", "/v1/user/wallet_holdings", {
chain,
wallet_address: walletAddress,
...extra,
}, null);
}
async getWalletActivity(
chain: string,
walletAddress: string,
extra: Record<string, string | number | string[]> = {}
): Promise<unknown> {
return this.authExistRequest("GET", "/v1/user/wallet_activity", {
chain,
wallet_address: walletAddress,
...extra,
});
}
async getWalletStats(chain: string, walletAddresses: string[], period = "7d"): Promise<unknown> {
return this.authExistRequest("GET", "/v1/user/wallet_stats", {
chain,
wallet_address: walletAddresses,
period,
});
}
async getWalletTokenBalance(
chain: string,
walletAddress: string,
tokenAddress: string
): Promise<unknown> {
return this.authExistRequest("GET", "/v1/user/wallet_token_balance", { chain, wallet_address: walletAddress, token_address: tokenAddress });
}
async getTrenches(chain: string, types?: string[], platforms?: string[], limit?: number, filters?: Record<string, number | string>): Promise<unknown> {
const body = buildTrenchesBody(chain, types, platforms, limit, filters);
return this.authExistRequest("POST", "/v1/trenches", { chain }, body);
}
// ---- Market trending endpoints (exist auth) ----
async getTrendingSwaps(
chain: string,
interval: string,
extra: Record<string, string | number | string[]> = {}
): Promise<unknown> {
return this.authExistRequest("GET", "/v1/market/rank", { chain, interval, ...extra });
}
async getTokenSignalV2(chain: string, groups: TokenSignalGroup[]): Promise<unknown> {
return this.authExistRequest("POST", "/v1/market/token_signal", {}, { chain, groups });
}
async getHotSearches(params: HotSearchesParam[]): Promise<unknown> {
return this.authExistRequest("POST", "/v1/market/hot_searches", {}, { params });
}
// ---- User endpoints (exist auth) ----
async getUserInfo(): Promise<unknown> {
return this.authExistRequest("GET", "/v1/user/info", {});
}
async getFollowWallet(chain: string, extra: Record<string, string | number | string[]> = {}): Promise<unknown> {
return this.authSignedRequest("GET", "/v1/trade/follow_wallet", { chain, ...extra }, null);
}
async getFollowTokens(chain: string, walletAddress: string, extra: Record<string, string | number> = {}): Promise<unknown> {
return this.authExistRequest("GET", "/v1/user/follow_tokens", { chain, wallet_address: walletAddress, ...extra });
}
async getFollowGroupNames(chain: string, walletAddress: string): Promise<unknown> {
return this.authExistRequest("GET", "/v1/user/follow_token_groups", { chain, wallet_address: walletAddress });
}
async getKol(chain?: string, limit?: number): Promise<unknown> {
const query: Record<string, string | number> = {};
if (chain) query["chain"] = chain;
if (limit != null) query["limit"] = limit;
return this.authExistRequest("GET", "/v1/user/kol", query);
}
async getSmartMoney(chain?: string, limit?: number): Promise<unknown> {
const query: Record<string, string | number> = {};
if (chain) query["chain"] = chain;
if (limit != null) query["limit"] = limit;
return this.authExistRequest("GET", "/v1/user/smartmoney", query);
}
async getCreatedTokens(chain: string, walletAddress: string, extra: Record<string, string | number> = {}): Promise<unknown> {
return this.authExistRequest("GET", "/v1/user/created_tokens", { chain, wallet_address: walletAddress, ...extra });
}
async quoteOrder(
chain: string,
from_address: string,
input_token: string,
output_token: string,
input_amount: string,
slippage: number
): Promise<unknown> {
const query = { chain, from_address, input_token, output_token, input_amount, slippage };
return this.authExistRequest("GET", "/v1/trade/quote", query);
}
// ---- Swap endpoints (signed auth) ----
async swap(params: SwapParams): Promise<unknown> {
return this.authSignedRequest("POST", "/v1/trade/swap", {}, params);
}
async multiSwap(params: MultiSwapParams): Promise<unknown> {
return this.authSignedRequest("POST", "/v1/trade/multi_swap", {}, params);
}
async queryOrder(orderId: string, chain: string): Promise<unknown> {
return this.authSignedRequest("GET", "/v1/trade/query_order", { order_id: orderId, chain }, null);
}
async getGasPrice(chain: string): Promise<unknown> {
return this.authExistRequest("GET", "/v1/trade/gas_price", { chain });
}
// ---- Strategy order endpoints (signed auth) ----
async createStrategyOrder(params: StrategyCreateParams): Promise<unknown> {
return this.authSignedRequest("POST", "/v1/trade/strategy/create", {}, params);
}
async getStrategyOrders(chain: string, extra: Record<string, string | number> = {}): Promise<unknown> {
return this.authSignedRequest("GET", "/v1/trade/strategy/orders", { chain, ...extra }, null);
}
async cancelStrategyOrder(params: StrategyCancelParams): Promise<unknown> {
return this.authSignedRequest("POST", "/v1/trade/strategy/cancel", {}, params);
}
// ---- Cooking endpoints ----
async getCookingStatistics(): Promise<unknown> {
return this.authExistRequest("GET", "/v1/cooking/statistics", {});
}
async createToken(params: CreateTokenParams): Promise<unknown> {
return this.authSignedRequest("POST", "/v1/cooking/create_token", {}, params);
}
// ---- Internal methods ----
private async authExistRequest(
method: string,
subPath: string,
queryExtra: Record<string, string | number | string[]>,
body: unknown = null
): Promise<unknown> {
return this.executePreparedRequest(() => {
const { timestamp, client_id } = buildAuthQuery();
const query: Record<string, string | number | string[]> = { ...queryExtra, timestamp, client_id };
const url = buildUrl(`${this.host}${subPath}`, query);
const headers: Record<string, string> = {
"X-APIKEY": this.apiKey,
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
};
const bodyStr = body !== null ? JSON.stringify(body) : null;
return {
method,
subPath,
url,
headers,
body: bodyStr,
curlStr: formatCurl(method, url, headers, bodyStr),
};
}, true);
}
private async authSignedRequest(
method: string,
subPath: string,
queryExtra: Record<string, string | number | string[]>,
body: unknown
): Promise<unknown> {
if (!this.privateKeyPem) {
throw new Error("GMGN_PRIVATE_KEY is required for critical-auth commands (swap, order, follow-wallet, and portfolio holdings commands)");
}
return this.executePreparedRequest(() => {
const { timestamp, client_id } = buildAuthQuery();
const query: Record<string, string | number | string[]> = { ...queryExtra, timestamp, client_id };
const bodyStr = body !== null ? JSON.stringify(body) : "";
const message = buildMessage(subPath, query, bodyStr, timestamp);
const signature = sign(message, this.privateKeyPem!, detectAlgorithm(this.privateKeyPem!));
const url = buildUrl(`${this.host}${subPath}`, query);
const headers: Record<string, string> = {
"X-APIKEY": this.apiKey,
"X-Signature": signature,
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
};
return {
method,
subPath,
url,
headers,
body: bodyStr || null,
curlStr: formatCurl(method, url, headers, bodyStr || null),
};
}, method !== "POST");
}
private async executePreparedRequest(
prepare: () => PreparedRequest,
autoRetryOnRateLimit: boolean
): Promise<unknown> {
const maxAttempts = autoRetryOnRateLimit ? 2 : 1;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const request = prepare();
const res = await this.doFetch(
request.method,
request.subPath,
request.url,
request.headers,
request.body,
request.curlStr
);
try {
return this.parseResponse(request.method, request.subPath, res, request.curlStr);
} catch (err) {
const retryDelayMs = getRateLimitRetryDelayMs(err, attempt, maxAttempts, autoRetryOnRateLimit);
if (retryDelayMs == null) {
throw err;
}
if (process.env.GMGN_DEBUG) {
console.error(
`[gmgn-cli] ${request.method} ${request.subPath} hit rate limit, retrying once in ${Math.ceil(retryDelayMs / 1000)}s`
);
}
await sleep(retryDelayMs);
}
}
throw new Error("Unexpected retry loop exit");
}
private async doFetch(
method: string,
subPath: string,
url: string,
headers: Record<string, string>,
body: string | null,
curlStr: string
): Promise<Response> {
try {
return await fetch(url, { method, headers, body: body ?? undefined });
} catch (err: unknown) {
const cause = extractRootCause(err);
const errorCode = (cause as NodeJS.ErrnoException).code;
// Detect IPv4 unavailability errors
if (errorCode === "EADDRNOTAVAIL" || errorCode === "ENETUNREACH") {
throw new Error(
`Network unreachable (${errorCode}): Your system may not support IPv4. ` +
`Please check your network configuration or contact support.`
);
}
if (process.env.GMGN_DEBUG) console.error(`${curlStr}\n[error] fetch failed: ${cause}`);
throw new Error(`${method} ${subPath} fetch failed: ${cause}`);
}
}
private async parseResponse(
method: string,
path: string,
res: Response,
curlStr: string
): Promise<unknown> {
const fail = (msg: string, body: string | null = null): never => {
if (process.env.GMGN_DEBUG) {
console.error(`${curlStr}\n${formatResponse(res, body)}`);
}
throw new Error(msg);
};
const resetAtUnix = parseRateLimitReset(res.headers.get("x-ratelimit-reset"));
let text!: string;
try {
text = await res.text();
} catch (err) {
fail(`${method} ${path} failed: HTTP ${res.status} (failed to read response body: ${err})`);
}
let json!: ResponseEnvelope;
try {
json = JSON.parse(text);
} catch {
fail(`${method} ${path} failed: HTTP ${res.status} (non-JSON response)`, text);
}
if (json.code !== 0) {
if (process.env.GMGN_DEBUG) {
console.error(`${curlStr}\n${formatResponse(res, text)}`);
}
throw new OpenApiError({
method,
path,
status: res.status,
apiCode: json.code,
apiError: json.error,
apiMessage: json.message,
resetAtUnix,
});
}
return json.data;
}
}
function getRateLimitRetryDelayMs(
err: unknown,
attempt: number,
maxAttempts: number,
autoRetryOnRateLimit: boolean
): number | null {
if (!autoRetryOnRateLimit || attempt >= maxAttempts) {
return null;
}
if (!(err instanceof OpenApiError)) {
return null;
}
if (err.apiError !== "RATE_LIMIT_EXCEEDED" && err.apiError !== "RATE_LIMIT_BANNED") {
return null;
}
if (err.resetAtUnix == null) {
return null;
}
const waitMs = Math.max(err.resetAtUnix * 1000 - Date.now(), 0) + RATE_LIMIT_RETRY_BUFFER_MS;
return waitMs <= getAutoRetryMaxWaitMs() ? waitMs : null;
}
function parseRateLimitReset(raw: string | null): number | undefined {
if (raw == null || raw.trim() === "") {
return undefined;
}
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}
function getAutoRetryMaxWaitMs(): number {
const raw = process.env.GMGN_RATE_LIMIT_AUTO_RETRY_MAX_WAIT_MS;
if (!raw) {
return DEFAULT_RATE_LIMIT_AUTO_RETRY_MAX_WAIT_MS;
}
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_RATE_LIMIT_AUTO_RETRY_MAX_WAIT_MS;
}
function buildOpenApiErrorMessage(params: OpenApiErrorParams): string {
const parts = [`${params.method} ${params.path} failed: HTTP ${params.status}`];
if (params.apiCode != null) parts.push(`code=${params.apiCode}`);
if (params.apiError) parts.push(`error=${params.apiError}`);
if (params.apiMessage) parts.push(`message=${params.apiMessage}`);
let message = parts.join(" ");
if (params.status !== 429) {
return message;
}
const resetText = params.resetAtUnix != null
? formatRateLimitReset(params.resetAtUnix)
: "an unknown time";
if (params.apiError === "ERROR_RATE_LIMIT_BLOCKED") {
return `${message}. Repeated business errors triggered a temporary block until ${resetText}. Fix the underlying request before retrying.`;
}
if (params.apiError === "RATE_LIMIT_EXCEEDED" || params.apiError === "RATE_LIMIT_BANNED") {
return `${message}. Rate limit resets at ${resetText}. Stop sending requests before then; repeated requests can extend the ban by 5s up to 5 minutes.`;
}
return `${message}. Received HTTP 429; retry after ${resetText}.`;
}
function formatRateLimitReset(resetAtUnix: number): string {
const resetAt = new Date(resetAtUnix * 1000);
const remainingSeconds = Math.max(Math.ceil((resetAt.getTime() - Date.now()) / 1000), 0);
return `${formatLocalTimestamp(resetAt)} (~${remainingSeconds}s remaining)`;
}
function formatLocalTimestamp(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
const offsetMinutes = -date.getTimezoneOffset();
const sign = offsetMinutes >= 0 ? "+" : "-";
const absOffsetMinutes = Math.abs(offsetMinutes);
const offsetHours = String(Math.floor(absOffsetMinutes / 60)).padStart(2, "0");
const offsetMins = String(absOffsetMinutes % 60).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} GMT${sign}${offsetHours}:${offsetMins}`;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function formatResponse(res: Response, body: string | null): string {
const headerLines = [...res.headers.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
return `[response] HTTP ${res.status}\n${headerLines}\n\n${body ?? "(no body)"}`;
}
const REDACTED_HEADERS = new Set(["x-apikey"]);
function formatCurl(method: string, url: string, headers: Record<string, string>, body: string | null): string {
const headerArgs = Object.entries(headers)
.map(([k, v]) => ` -H '${k}: ${REDACTED_HEADERS.has(k.toLowerCase()) ? "***" : v}'`)
.join(" \\\n");
const bodyArg = body ? ` \\\n -d '${body.replace(/'/g, "'\\''")}'` : "";
return `\n[curl]\ncurl -X ${method} '${url}' \\\n${headerArgs}${bodyArg}\n`;
}
const TRENCHES_PLATFORMS: Record<string, string[]> = {
sol: [
"Pump.fun", "pump_mayhem", "pump_mayhem_agent", "pump_agent",
"letsbonk", "bonkers", "bags", "memoo", "liquid", "bankr", "zora",
"surge", "anoncoin", "moonshot_app", "wendotdev", "heaven", "sugar",
"token_mill", "believe", "trendsfun", "trends_fun", "jup_studio",
"Moonshot", "boop", "ray_launchpad", "meteora_virtual_curve", "xstocks",
],
bsc: [
"fourmeme", "fourmeme_agent", "bn_fourmeme", "four_xmode_agent",
"cubepeg", "likwid", "goplus_creator", "goplus_skills", "openfour",
"flap", "flap_stocks", "flap_aioracle", "clanker", "lunafun",
],
base: [
"clanker", "bankr", "flaunch", "zora", "zora_creator",
"baseapp", "basememe", "virtuals_v2", "klik",
],
eth: [
"trench", "clanker", "klik", "livo", "stroid",
"pool_uniswap_v2", "pool_uniswap_v3", "printr",
],
};
const TRENCHES_QUOTE_ADDRESS_TYPES: Record<string, number[]> = {
sol: [4, 5, 3, 1, 13, 0],
bsc: [6, 7, 1, 16, 8, 3, 9, 10, 2, 17, 18, 0],
base: [11, 3, 12, 13, 0],
eth: [20, 11, 8, 3, 12, 1, 0],
};
function buildTrenchesBody(chain: string, types?: string[], platforms?: string[], limit?: number, filters?: Record<string, number | string>): Record<string, unknown> {
const selectedTypes = types?.length ? types : ["new_creation", "near_completion", "completed"];
const launchpad_platform = platforms?.length ? platforms : (TRENCHES_PLATFORMS[chain] ?? []);
const quote_address_type = TRENCHES_QUOTE_ADDRESS_TYPES[chain] ?? [];
const actualLimit = limit ?? 80;
const section: Record<string, unknown> = {
filters: ["offchain", "onchain"],
launchpad_platform,
quote_address_type,
launchpad_platform_v2: true,
limit: actualLimit,
...filters,
};
const body: Record<string, unknown> = { version: "v2" };
for (const type of selectedTypes) {
body[type] = { ...section };
}
return body;
}
function buildUrl(base: string, query: Record<string, string | number | string[]>): string {
const params = new URLSearchParams();
for (const [k, v] of Object.entries(query)) {
if (Array.isArray(v)) {
for (const item of v) params.append(k, item);
} else {
params.set(k, String(v));
}
}
return `${base}?${params.toString()}`;
}
// Recursively extract the root cause from nested Error.cause chain
function extractRootCause(err: unknown): unknown {
if (err instanceof Error && err.cause) {
return extractRootCause(err.cause);
}
return err;
}