diff --git a/CLAUDE.md b/CLAUDE.md index 9551360..8369c16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,8 +93,8 @@ EOF | Mode | Commands | Requirements | |------|----------|--------------| -| Normal | token / market / portfolio | `GMGN_API_KEY` only, no signature | -| Critical | swap / order | `GMGN_API_KEY` + `GMGN_PRIVATE_KEY` — CLI handles signing automatically | +| Normal | token / market / portfolio / track kol / track smartmoney | `GMGN_API_KEY` only, no signature | +| Critical | swap / order / track follow-wallet | `GMGN_API_KEY` + `GMGN_PRIVATE_KEY` — CLI handles signing automatically | ## SKILL.md Authoring Rules diff --git a/skills/gmgn-track/SKILL.md b/skills/gmgn-track/SKILL.md index 98e816a..7b167d0 100644 --- a/skills/gmgn-track/SKILL.md +++ b/skills/gmgn-track/SKILL.md @@ -60,7 +60,8 @@ Use the `gmgn-cli` tool to query on-chain tracking data based on the user's requ ## Prerequisites - `gmgn-cli` installed globally — if missing, run: `npm install -g gmgn-cli` -- `GMGN_API_KEY` configured in `~/.config/gmgn/.env` — required for all sub-commands; no private key needed +- `GMGN_API_KEY` configured in `~/.config/gmgn/.env` — required for all sub-commands +- `GMGN_PRIVATE_KEY` — required for `track follow-wallet` only (critical auth); not needed for `kol` or `smartmoney` ## Rate Limit Handling @@ -88,11 +89,13 @@ When a request returns `429`: ``` Tell the user: *"This is your Ed25519 public key. Go to **https://gmgn.ai/ai**, paste it into the API key creation form, then send me the API Key value shown on the page."* -2. Wait for the user's API key, then configure: +2. Wait for the user's API key, then configure (saves both API key and private key — private key is required for `track follow-wallet`): ```bash mkdir -p ~/.config/gmgn - echo 'GMGN_API_KEY=' > ~/.config/gmgn/.env + echo "GMGN_API_KEY=" > ~/.config/gmgn/.env + echo "GMGN_PRIVATE_KEY=$(awk '{printf "%s\\n", $0}' /tmp/gmgn_private.pem)" >> ~/.config/gmgn/.env chmod 600 ~/.config/gmgn/.env + rm /tmp/gmgn_private.pem ``` ## Usage Examples @@ -308,7 +311,7 @@ To research any token surfaced by smart money activity, follow [`docs/workflow-t ## Notes -- All sub-commands use normal auth (API Key only, no signature required) +- `track follow-wallet` uses critical auth (API Key + private key signature); `track kol` and `track smartmoney` use normal auth (API Key only) - `track follow-wallet` returns trades from wallets followed on the GMGN platform; the follow list is resolved automatically from the GMGN user account bound to the API Key — `--wallet` is optional - Use `--raw` to get single-line JSON for further processing - `track kol` / `track smartmoney` `--side` is a **client-side filter** — the CLI fetches all results then filters locally; it is NOT sent to the API diff --git a/src/client/OpenApiClient.ts b/src/client/OpenApiClient.ts index 9aecd84..fc7f041 100644 --- a/src/client/OpenApiClient.ts +++ b/src/client/OpenApiClient.ts @@ -307,7 +307,7 @@ export class OpenApiClient { } async getFollowWallet(chain: string, extra: Record = {}): Promise { - return this.normalRequest("GET", "/v1/trade/follow_wallet", { chain, ...extra }); + return this.criticalRequest("GET", "/v1/trade/follow_wallet", { chain, ...extra }, null); } async getKol(chain?: string, limit?: number): Promise { @@ -409,16 +409,16 @@ export class OpenApiClient { private async criticalRequest( method: string, subPath: string, - queryExtra: Record, + queryExtra: Record, body: unknown ): Promise { if (!this.privateKeyPem) { - throw new Error("GMGN_PRIVATE_KEY is required for critical-auth commands (swap and all order commands)"); + throw new Error("GMGN_PRIVATE_KEY is required for critical-auth commands (swap, order, and follow-wallet commands)"); } return this.executePreparedRequest(() => { const { timestamp, client_id } = buildAuthQuery(); - const query: Record = { ...queryExtra, timestamp, client_id }; + const query: Record = { ...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!)); diff --git a/src/client/signer.ts b/src/client/signer.ts index cf6ffe4..d17ca3a 100644 --- a/src/client/signer.ts +++ b/src/client/signer.ts @@ -32,17 +32,24 @@ export function buildAuthQuery(): { timestamp: number; client_id: string } { /** * Build the signature message (critical auth) * Format: {sub_path}:{sorted_query_string}:{request_body}:{timestamp} - * sorted_query_string: all query params (including timestamp, client_id) sorted alphabetically by key + * sorted_query_string: all query params (including timestamp, client_id) sorted alphabetically by key. + * Array values are serialized as repeated k=v pairs (same as buildUrl / URLSearchParams), sorted by value. */ export function buildMessage( subPath: string, - queryParams: Record, + queryParams: Record, body: string, timestamp: number ): string { const sortedQs = Object.keys(queryParams) .sort() - .map((k) => `${k}=${queryParams[k]}`) + .flatMap((k) => { + const v = queryParams[k]; + if (Array.isArray(v)) { + return [...v].sort().map((item) => `${k}=${item}`); + } + return [`${k}=${v}`]; + }) .join("&"); return `${subPath}:${sortedQs}:${body}:${timestamp}`; } diff --git a/src/config.ts b/src/config.ts index a8418db..7bfb0f8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,7 +15,7 @@ export interface Config { let _config: Config | null = null; const PRIVATE_KEY_REQUIRED_MSG = - "GMGN_PRIVATE_KEY is required for critical-auth commands (swap and all order commands)"; + "GMGN_PRIVATE_KEY is required for critical-auth commands (swap, order, and follow-wallet commands)"; export function getConfig(requirePrivateKey = false): Config { if (_config) {