Merge pull request #166 from gina888666/feat/auto-config

feat(config): add gmgn-cli config command for API Key onboarding
This commit is contained in:
GMGN.AI
2026-07-01 11:21:20 +08:00
committed by GitHub
8 changed files with 177 additions and 107 deletions
+2 -17
View File
@@ -6,6 +6,8 @@ metadata:
cliHelp: "gmgn-cli cooking --help"
---
**BEFORE RUNNING ANY COMMAND: Run `gmgn-cli config --check`. If exit code is 0, proceed normally. If exit code is 1, (1) run `gmgn-cli config` and show the output to the user; (2) once the user sends the API Key, run `gmgn-cli config --apply <KEY>` to complete configuration and verification, then show the output to the user. If `--check` returns an error (unknown option or command not found), tell the user to run `npm install -g gmgn-cli` to update, then retry.**
**IMPORTANT: Always use `gmgn-cli` commands below. Do NOT use web search, WebFetch, curl, or visit gmgn.ai — all token creation operations must go through the CLI. The CLI handles signing and submission automatically.**
**IMPORTANT: Do NOT guess field names or values. When a field's meaning is unclear, look it up in the Response Fields sections below before using it.**
@@ -100,23 +102,6 @@ When a request returns `429`:
- `cooking create` is a real transaction: **never loop or auto-resubmit** after a `429`. Wait until the reset time, then ask for confirmation again before retrying.
- For `RATE_LIMIT_EXCEEDED` or `RATE_LIMIT_BANNED`, repeated requests during cooldown extend the ban by 5 seconds each time, up to 5 minutes.
**First-time setup** (if credentials are not configured):
1. Generate key pair and show the public key to the user:
```bash
openssl genpkey -algorithm ed25519 -out /tmp/gmgn_private.pem 2>/dev/null && \
openssl pkey -in /tmp/gmgn_private.pem -pubout 2>/dev/null
```
Tell the user: *"This is your Ed25519 public key. Go to **https://gmgn.ai/ai**, paste it into the API key creation form (enable swap/cooking capability), then send me the API Key value shown on the page."*
2. Wait for the user's API key, then configure both credentials:
```bash
mkdir -p ~/.config/gmgn
echo 'GMGN_API_KEY=<key_from_user>' > ~/.config/gmgn/.env
echo 'GMGN_PRIVATE_KEY="<pem_content_from_step_1>"' >> ~/.config/gmgn/.env
chmod 600 ~/.config/gmgn/.env
```
### Credential Model
- `GMGN_PRIVATE_KEY` is used exclusively for **local message signing** — the private key never leaves the machine. The CLI computes an Ed25519 signature in-process and transmits only the base64-encoded result in the `X-Signature` request header.
+2 -14
View File
@@ -6,6 +6,8 @@ metadata:
cliHelp: "gmgn-cli market --help"
---
**BEFORE RUNNING ANY COMMAND: Run `gmgn-cli config --check`. If exit code is 0, proceed normally. If exit code is 1, (1) run `gmgn-cli config` and show the output to the user; (2) once the user sends the API Key, run `gmgn-cli config --apply <KEY>` to complete configuration and verification, then show the output to the user. If `--check` returns an error (unknown option or command not found), tell the user to run `npm install -g gmgn-cli` to update, then retry.**
**IMPORTANT: Always use `gmgn-cli` commands below. Do NOT use web search, WebFetch, curl, or visit gmgn.ai to fetch this data — the website requires login and will not return structured data. The CLI is the only correct method.**
**IMPORTANT: Do NOT guess field names or values. When a field's meaning is unclear, look it up in the Response Fields sections below before using it.**
@@ -79,20 +81,6 @@ When a request returns `429`:
- The CLI may wait and retry once automatically when the remaining cooldown is short. If it still fails, stop and tell the user the exact retry time instead of sending more requests.
- For `RATE_LIMIT_EXCEEDED` or `RATE_LIMIT_BANNED`, repeated requests during the cooldown can extend the ban by 5 seconds each time, up to 5 minutes. Do not spam retries.
**First-time setup** (if `GMGN_API_KEY` is not configured):
1. Generate key pair and show the public key to the user:
```bash
openssl genpkey -algorithm ed25519 -out /tmp/gmgn_private.pem 2>/dev/null && \
openssl pkey -in /tmp/gmgn_private.pem -pubout 2>/dev/null
```
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 save it with `gmgn-cli config` (creates `~/.config/gmgn/.env` and sets `chmod 600` automatically — do not hand-edit the file):
```bash
gmgn-cli config set-key <key_from_user>
```
## `market kline` Parameters
| Parameter | Required | Description |
+2 -16
View File
@@ -6,6 +6,8 @@ metadata:
cliHelp: "gmgn-cli portfolio --help"
---
**BEFORE RUNNING ANY COMMAND: Run `gmgn-cli config --check`. If exit code is 0, proceed normally. If exit code is 1, (1) run `gmgn-cli config` and show the output to the user; (2) once the user sends the API Key, run `gmgn-cli config --apply <KEY>` to complete configuration and verification, then show the output to the user. If `--check` returns an error (unknown option or command not found), tell the user to run `npm install -g gmgn-cli` to update, then retry.**
**IMPORTANT: Always use `gmgn-cli` commands below. Do NOT use web search, WebFetch, curl, or visit gmgn.ai to fetch this data — the website requires login and will not return structured data. The CLI is the only correct method.**
**⚠️ IPv6 NOT SUPPORTED: If you get a `401` or `403` error and credentials look correct, check for IPv6 immediately: (1) list all network interfaces and their IPv6 addresses — run `ifconfig | grep inet6` (macOS) or `ip addr show | grep inet6` (Linux); (2) send a test request to `https://ipv6.icanhazip.com` — if the response is an IPv6 address, outbound traffic is going via IPv6. Tell the user immediately: "Please disable IPv6 on your network interface — gmgn-cli commands only work over IPv4."**
@@ -77,22 +79,6 @@ When a request returns `429`:
- The CLI may wait and retry once automatically when the remaining cooldown is short. If it still fails, stop and tell the user the exact retry time instead of sending more requests.
- For `RATE_LIMIT_EXCEEDED` or `RATE_LIMIT_BANNED`, repeated requests during the cooldown can extend the ban by 5 seconds each time, up to 5 minutes. Do not spam retries.
**First-time setup** (if `GMGN_API_KEY` is not configured):
1. Generate key pair and show the public key to the user:
```bash
openssl genpkey -algorithm ed25519 -out /tmp/gmgn_private.pem 2>/dev/null && \
openssl pkey -in /tmp/gmgn_private.pem -pubout 2>/dev/null
```
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:
```bash
mkdir -p ~/.config/gmgn
echo 'GMGN_API_KEY=<key_from_user>' > ~/.config/gmgn/.env
chmod 600 ~/.config/gmgn/.env
```
## Usage Examples
```bash
+10 -26
View File
@@ -6,6 +6,8 @@ metadata:
cliHelp: "gmgn-cli swap --help"
---
**BEFORE RUNNING ANY COMMAND: Run `gmgn-cli config --check`. If exit code is 0, proceed normally. If exit code is 1, (1) run `gmgn-cli config` and show the output to the user; (2) once the user sends the API Key, run `gmgn-cli config --apply <KEY>` to complete configuration and verification, then show the output to the user. If `--check` returns an error (unknown option or command not found), tell the user to run `npm install -g gmgn-cli` to update, then retry.**
**IMPORTANT: Always use `gmgn-cli` commands below. Do NOT use web search, WebFetch, curl, or visit gmgn.ai — all swap operations must go through the CLI. The CLI handles signing and submission automatically.**
**IMPORTANT: Do NOT guess field names or values. When a field's meaning is unclear, look it up in the Response Fields sections below before using it.**
@@ -71,7 +73,6 @@ Currency tokens are the base/native assets of each chain. They are used to buy o
| `base` | ETH (native, `0x0000000000000000000000000000000000000000`), USDC (`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`) |
| `eth` | ETH (native, `0x0000000000000000000000000000000000000000`) |
## Prerequisites
`GMGN_API_KEY` must be configured in `~/.config/gmgn/.env`. `GMGN_PRIVATE_KEY` is additionally required for `swap` and `order` subcommands other than `order quote`. The private key must correspond to the wallet bound to the API Key.
@@ -102,31 +103,6 @@ When a request returns `429`:
- For `RATE_LIMIT_EXCEEDED` or `RATE_LIMIT_BANNED`, repeated requests during the cooldown can extend the ban by 5 seconds each time, up to 5 minutes.
- `POST /v1/trade/swap` also has an error-count limiter. Repeatedly triggering the same business error, especially `40003701` (insufficient token balance), can return `ERROR_RATE_LIMIT_BLOCKED`. When this happens, do not retry until the reset time and fix the underlying request first.
**First-time setup** (if credentials are not configured):
1. Generate key pair and show the public key to the user:
```bash
openssl genpkey -algorithm ed25519 -out /tmp/gmgn_private.pem 2>/dev/null && \
openssl pkey -in /tmp/gmgn_private.pem -pubout 2>/dev/null
```
Tell the user: *"This is your Ed25519 public key. Go to **https://gmgn.ai/ai**, paste it into the API key creation form (enable swap capability), then send me the API Key value shown on the page."*
2. Wait for the user's API key, then configure both credentials:
```bash
mkdir -p ~/.config/gmgn
echo 'GMGN_API_KEY=<key_from_user>' > ~/.config/gmgn/.env
echo 'GMGN_PRIVATE_KEY="<pem_content_from_step_1>"' >> ~/.config/gmgn/.env
chmod 600 ~/.config/gmgn/.env
```
### Credential Model
- Both `GMGN_API_KEY` and `GMGN_PRIVATE_KEY` are read from the `.env` file by the CLI at startup. They are **never passed as command-line arguments** and never appear in shell command strings.
- `GMGN_PRIVATE_KEY` is used exclusively for **local message signing** — the private key never leaves the machine. The CLI computes an Ed25519 or RSA-SHA256 signature in-process and transmits only the base64-encoded result in the `X-Signature` request header.
- `GMGN_API_KEY` is transmitted in the `X-APIKEY` request header to GMGN's servers over HTTPS.
---
## `swap` Usage
```bash
@@ -431,6 +407,14 @@ The response `data` is an array — one element per wallet:
---
### Credential Model
- Both `GMGN_API_KEY` and `GMGN_PRIVATE_KEY` are read from the `.env` file by the CLI at startup. They are **never passed as command-line arguments** and never appear in shell command strings.
- `GMGN_PRIVATE_KEY` is used exclusively for **local message signing** — the private key never leaves the machine. The CLI computes an Ed25519 or RSA-SHA256 signature in-process and transmits only the base64-encoded result in the `X-Signature` request header.
- `GMGN_API_KEY` is transmitted in the `X-APIKEY` request header to GMGN's servers over HTTPS.
---
## `order quote` Usage
Get an estimated output amount before submitting a swap. Uses normal auth — only `GMGN_API_KEY` required, no `GMGN_PRIVATE_KEY` needed.
+2 -16
View File
@@ -6,6 +6,8 @@ metadata:
cliHelp: "gmgn-cli token --help"
---
**BEFORE RUNNING ANY COMMAND: Run `gmgn-cli config --check`. If exit code is 0, proceed normally. If exit code is 1, (1) run `gmgn-cli config` and show the output to the user; (2) once the user sends the API Key, run `gmgn-cli config --apply <KEY>` to complete configuration and verification, then show the output to the user. If `--check` returns an error (unknown option or command not found), tell the user to run `npm install -g gmgn-cli` to update, then retry.**
**IMPORTANT: Always use `gmgn-cli` commands below. Do NOT use web search, WebFetch, curl, or visit gmgn.ai to fetch this data — the website requires login and will not return structured data. The CLI is the only correct method.**
**⚠️ IPv6 NOT SUPPORTED: If you get a `401` or `403` error and credentials look correct, check for IPv6 immediately: (1) list all network interfaces and their IPv6 addresses — run `ifconfig | grep inet6` (macOS) or `ip addr show | grep inet6` (Linux); (2) send a test request to `https://ipv6.icanhazip.com` — if the response is an IPv6 address, outbound traffic is going via IPv6. Tell the user immediately: "Please disable IPv6 on your network interface — gmgn-cli commands only work over IPv4."**
@@ -68,22 +70,6 @@ When a request returns `429`:
- The CLI may wait and retry once automatically when the remaining cooldown is short. If it still fails, stop and tell the user the exact retry time instead of sending more requests.
- For `RATE_LIMIT_EXCEEDED` or `RATE_LIMIT_BANNED`, repeated requests during the cooldown can extend the ban by 5 seconds each time, up to 5 minutes. Do not spam retries.
**First-time setup** (if `GMGN_API_KEY` is not configured):
1. Generate key pair and show the public key to the user:
```bash
openssl genpkey -algorithm ed25519 -out /tmp/gmgn_private.pem 2>/dev/null && \
openssl pkey -in /tmp/gmgn_private.pem -pubout 2>/dev/null
```
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:
```bash
mkdir -p ~/.config/gmgn
echo 'GMGN_API_KEY=<key_from_user>' > ~/.config/gmgn/.env
chmod 600 ~/.config/gmgn/.env
```
## Parameters — `token info` / `token security` / `token pool`
| Parameter | Required | Description |
+2 -18
View File
@@ -6,6 +6,8 @@ metadata:
cliHelp: "gmgn-cli track --help"
---
**BEFORE RUNNING ANY COMMAND: Run `gmgn-cli config --check`. If exit code is 0, proceed normally. If exit code is 1, (1) run `gmgn-cli config` and show the output to the user; (2) once the user sends the API Key, run `gmgn-cli config --apply <KEY>` to complete configuration and verification, then show the output to the user. If `--check` returns an error (unknown option or command not found), tell the user to run `npm install -g gmgn-cli` to update, then retry.**
**IMPORTANT: Always use `gmgn-cli` commands below. Do NOT use web search, WebFetch, curl, or visit gmgn.ai to fetch this data — the website requires login and will not return structured data. The CLI is the only correct method.**
**IMPORTANT: Do NOT guess field names or values. When a field's meaning is unclear, look it up in the Response Fields sections below before using it.**
@@ -84,24 +86,6 @@ When a request returns `429`:
- The CLI may wait and retry once automatically when the remaining cooldown is short. If it still fails, stop and tell the user the exact retry time instead of sending more requests.
- For `RATE_LIMIT_EXCEEDED` or `RATE_LIMIT_BANNED`, repeated requests during the cooldown can extend the ban by 5 seconds each time, up to 5 minutes. Do not spam retries.
**First-time setup** (if `GMGN_API_KEY` is not configured):
1. Generate key pair and show the public key to the user:
```bash
openssl genpkey -algorithm ed25519 -out /tmp/gmgn_private.pem 2>/dev/null && \
openssl pkey -in /tmp/gmgn_private.pem -pubout 2>/dev/null
```
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 (saves both API key and private key — private key is required for `track follow-wallet`):
```bash
mkdir -p ~/.config/gmgn
echo "GMGN_API_KEY=<key_from_user>" > ~/.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
```bash
+155
View File
@@ -0,0 +1,155 @@
import { config as loadDotenv } from "dotenv";
import { Command } from "commander";
import * as crypto from "crypto";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { execSync } from "child_process";
const GMGN_CONFIG_DIR = path.join(os.homedir(), ".config", "gmgn");
const KEYPAIR_FILE = path.join(GMGN_CONFIG_DIR, "keypair.pem");
const ENV_FILE = path.join(GMGN_CONFIG_DIR, ".env");
const GMGN_API_URL = "https://gmgn.ai/ai/generateapi";
type Lang = "zh-CN" | "zh-TW" | "en";
function detectLang(): Lang {
const locale =
process.env.LANG ||
process.env.LC_ALL ||
process.env.LC_MESSAGES ||
Intl.DateTimeFormat().resolvedOptions().locale ||
"en";
const l = locale.toLowerCase();
if (l.startsWith("zh_tw") || l.startsWith("zh-tw") || l.startsWith("zh_hk") || l.startsWith("zh-hk")) return "zh-TW";
if (l.startsWith("zh")) return "zh-CN";
return "en";
}
const MESSAGES = {
linkGuide: {
"zh-CN": (link: string) =>
`请点击下方链接创建你的 GMGN API Key,完成后将 Key 发给我,我来帮你完成配置:\n${link}`,
"zh-TW": (link: string) =>
`請點擊下方連結建立你的 GMGN API Key,完成後將 Key 發給我,我來幫你完成配置:\n${link}`,
en: (link: string) =>
`Please click the link below to create your GMGN API Key. Once created, send me the API Key and I will finish the configuration:\n${link}`,
},
verifySuccess: {
"zh-CN": "配置验证成功,可以开始使用了。",
"zh-TW": "配置驗證成功,可以開始使用了。",
en: "Configuration verified successfully. You are ready to use GMGN.",
},
verifyFail: {
"zh-CN":
"配置验证失败:API Key 与本地密钥不匹配。\n请确认:\n1. API Key 是否填写正确;\n2. 创建 API Key 时,是否使用的是页面自动填入的公钥。",
"zh-TW":
"配置驗證失敗:API Key 與本地密鑰不匹配。\n請確認:\n1. API Key 是否填寫正確;\n2. 創建 API Key 時,是否使用的是頁面自動填入的公鑰。",
en: "Configuration verification failed: API Key does not match your local key pair.\nPlease confirm:\n1. Whether the API Key was entered correctly.\n2. Whether you used the public key that was pre-filled on the page when creating the API Key.",
},
verifyNetworkFail: {
"zh-CN": "配置已写入,但验证请求失败(可能是网络问题)。你可以先尝试使用,如遇接口报错再重新配置。",
"zh-TW": "配置已寫入,但驗證請求失敗(可能是網路問題)。你可以先嘗試使用,如遇介面報錯再重新配置。",
en: "Configuration saved, but the verification request failed (possibly a network issue). You can try using it now and reconfigure if you encounter API errors.",
},
};
function getOrCreateKeypair(): string {
fs.mkdirSync(GMGN_CONFIG_DIR, { recursive: true });
if (fs.existsSync(KEYPAIR_FILE)) {
const content = fs.readFileSync(KEYPAIR_FILE, "utf-8");
const match = content.match(/(-----BEGIN PUBLIC KEY-----[\s\S]+?-----END PUBLIC KEY-----)/);
if (!match) {
console.error("Error: keypair.pem exists but public key could not be parsed. Delete ~/.config/gmgn/keypair.pem and try again.");
process.exit(1);
}
return match[1] + "\n";
}
const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
const privatePem = privateKey.export({ type: "pkcs8", format: "pem" }) as string;
const publicPem = publicKey.export({ type: "spki", format: "pem" }) as string;
const entry = `# Private Key\n${privatePem}\n# Public Key\n${publicPem}\n`;
fs.writeFileSync(KEYPAIR_FILE, entry, { mode: 0o600 });
return publicPem;
}
function writeEnv(apiKey: string, privatePem: string): void {
const pkOneLine = privatePem.replace(/\r?\n/g, "\\n");
let existing = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, "utf-8") : "";
existing = existing
.split("\n")
.filter((l) => !/^GMGN_API_KEY=|^GMGN_PRIVATE_KEY=/.test(l))
.join("\n")
.trim();
const content = (existing ? existing + "\n" : "") + `GMGN_API_KEY=${apiKey}\nGMGN_PRIVATE_KEY=${pkOneLine}\n`;
fs.mkdirSync(GMGN_CONFIG_DIR, { recursive: true });
fs.writeFileSync(ENV_FILE, content, { mode: 0o600 });
}
function verify(): "ok" | "auth_fail" | "network_fail" {
try {
execSync("gmgn-cli track follow-wallet --chain sol --limit 1", { stdio: "pipe" });
return "ok";
} catch (e: unknown) {
const output = [
(e as any).stderr?.toString() ?? "",
(e as any).stdout?.toString() ?? "",
].join(" ");
if (/401|403|unauthorized|forbidden|invalid.*key|key.*invalid|signature/i.test(output)) {
return "auth_fail";
}
return "network_fail";
}
}
export function registerConfigCommands(program: Command): void {
const cmd = program
.command("config")
.description("Generate an Ed25519 key pair and output a pre-filled GMGN API Key creation link, or apply an API Key");
cmd
.option("--check", "Check if GMGN_API_KEY is configured (exit 0 = found, exit 1 = not found)")
.option("--apply <api_key>", "Write API Key + private key to ~/.config/gmgn/.env and verify")
.action(async (opts) => {
const lang = detectLang();
if (opts.check) {
loadDotenv({ path: ENV_FILE, override: true });
loadDotenv();
process.exit(process.env.GMGN_API_KEY ? 0 : 1);
}
if (opts.apply) {
// --apply: read private key from keypair.pem, write .env, verify
if (!fs.existsSync(KEYPAIR_FILE)) {
console.error("Error: ~/.config/gmgn/keypair.pem not found. Run `gmgn-cli config` first to generate a key pair.");
process.exit(1);
}
const content = fs.readFileSync(KEYPAIR_FILE, "utf-8");
const match = content.match(/(-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----)/);
if (!match) {
console.error("Error: keypair.pem exists but private key could not be parsed. Delete ~/.config/gmgn/keypair.pem and run `gmgn-cli config` again.");
process.exit(1);
}
writeEnv(opts.apply, match[1]);
const result = verify();
if (result === "ok") {
console.log(MESSAGES.verifySuccess[lang]);
} else if (result === "auth_fail") {
console.error(MESSAGES.verifyFail[lang]);
process.exit(1);
} else {
console.log(MESSAGES.verifyNetworkFail[lang]);
}
return;
}
// Default: generate/reuse keypair, output link with guidance
const publicPem = getOrCreateKeypair();
const link = `${GMGN_API_URL}?pbk=${encodeURIComponent(publicPem)}`;
console.log(MESSAGES.linkGuide[lang](link));
});
}
+2
View File
@@ -11,6 +11,7 @@ import { registerPortfolioCommands } from "./commands/portfolio.js";
import { registerTrackCommands } from "./commands/track.js";
import { registerSwapCommands } from "./commands/swap.js";
import { registerCookingCommands } from "./commands/cooking.js";
import { registerConfigCommands } from "./commands/config.js";
const proxy = process.env.HTTPS_PROXY ?? process.env.https_proxy
?? process.env.HTTP_PROXY ?? process.env.http_proxy;
@@ -60,6 +61,7 @@ registerPortfolioCommands(program);
registerTrackCommands(program);
registerSwapCommands(program);
registerCookingCommands(program);
registerConfigCommands(program);
program.parseAsync().catch((err) => {
console.error(`[gmgn-cli] ${err.message}`);