Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ DODO_PROJECT_ID=
# DODO route API endpoint (rarely needs changing)
# DODO_API_URL=https://api.dodoex.io/route-service/v2/widget/getdodoroute

# Optional token metadata and market data sources used by `token info`.
# CoinGecko and DexScreener work without keys, subject to their public rate limits.
# COINGECKO_API_URL=https://api.coingecko.com/api/v3
# COINGECKO_API_KEY=
# DEXSCREENER_API_URL=https://api.dexscreener.com/latest/dex

# ── Wallet ────────────────────────────────────────────────────────────────────
# Private key used for signing transactions.
# PRIVATE_KEY=0x...
Expand Down
24 changes: 20 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,29 @@ jobs:
fi

if [[ "$is_main" == "true" && "$prerelease" == "true" ]]; then
echo "main branch cannot publish prerelease versions: $version" >&2
exit 1
echo "main branch cannot publish prerelease versions: $version — skipping release"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "branch_name=$branch_name" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "cargo_version=$cargo_version" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
echo "stable=$stable" >> "$GITHUB_OUTPUT"
echo "previous_tag=" >> "$GITHUB_OUTPUT"
exit 0
fi

if [[ "$is_main" != "true" && "$stable" == "true" ]]; then
echo "non-main branch must use prerelease versions with a suffix: $version" >&2
exit 1
echo "non-main branch must use prerelease versions with a suffix: $version — skipping release"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "branch_name=$branch_name" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "cargo_version=$cargo_version" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
echo "stable=$stable" >> "$GITHUB_OUTPUT"
echo "previous_tag=" >> "$GITHUB_OUTPUT"
exit 0
fi

if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "chain-pilot"
version = "0.2.0"
version = "0.3.0"
description = "A CLI tool for on-chain DeFi operations on EVM-compatible networks"
homepage = "https://github.com/DODOEX/ChainPilot"
documentation = "https://github.com/DODOEX/ChainPilot#readme"
Expand Down
170 changes: 160 additions & 10 deletions skills/chainpilot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,16 @@ the user explicitly asks for a different one.

Runtime env vars are intentionally limited to `PRIVATE_KEY`, `KEYSTORE_PATH`,
`KEYSTORE_PASSWORD_FILE`, `KEYSTORE_PASSWORD_ENV`, `KEYSTORE_PASSWORD`,
`WALLET_ADDRESS`, `CHAIN_ID`, `DODO_API_KEY`, `DODO_PROJECT_ID`, and
`DODO_API_URL`.
`WALLET_ADDRESS`, `CHAIN_ID`, `DODO_API_KEY`, `DODO_PROJECT_ID`,
`DODO_API_URL`, `COINGECKO_API_URL`, `COINGECKO_API_KEY`, and
`DEXSCREENER_API_URL`.

Config precedence: CLI flag > env var > `.env` file > compile-time default.
Runtime config precedence: CLI flag > existing environment variable / `.env` file
> persistent `config.env` file > compile-time default.

Use `chainpilot config set` for supported API keys instead of asking the user to
paste secrets into shell commands. `config.env` is stored in ChainPilot's local
data directory and sensitive values are masked by `config get` / `config list`.

If `--keystore-path` is set, password resolution order is:

Expand Down Expand Up @@ -182,8 +188,20 @@ lookups can fall back to this local store when the DODO tokenlist does not have
the symbol.

**Token not found handling**: If `chainpilot swap quote` returns an error indicating
the token symbol was not found, use the Coingecko API to search for the token's
contract address on the target chain:
the token symbol was not found, first try the built-in token search surfaced by
the metadata commands:

```bash
chainpilot [--chain-id <N>] token info <SYMBOL>
```

If candidates are returned, show the candidate address, chain, source, and
liquidity to the user and require explicit confirmation before retrying the swap
with an address. Never pass an externally sourced address directly into a swap
command without the user first approving it.

If the CLI returns no candidates, optionally use the CoinGecko API to search for
the token's contract address on the target chain:

```bash
# Search token address via Coingecko
Expand All @@ -192,8 +210,7 @@ curl -s "https://api.coingecko.com/api/v3/search?query=<SYMBOL>" | jq '.coins[]

Then show the found address (filtered by target chain, e.g. `ethereum`, `polygon`,
`arbitrum`, `base`, `bnb`) to the user and **require explicit confirmation**
before retrying with the address. Never pass a CoinGecko-sourced address
directly into a command without the user first approving it.
before retrying with the address.

### `swap simulate`

Expand Down Expand Up @@ -292,8 +309,12 @@ chainpilot swap history [--limit <N>] [--status pending|success|failed]
chainpilot [--chain-id <N>] token info <TOKEN>
```

ERC-20 metadata: name, symbol, decimals, total supply.
`<TOKEN>` can be a symbol (`USDC`) or contract address (`0x...`).
Token metadata from on-chain reads plus external enrichment. Output can include
name, symbol, decimals, chain, website/social links, price, market cap, FDV,
liquidity, volume, 24h price change, risk level, and per-field sources.
`<TOKEN>` can be a symbol (`USDC`), native token symbol (`ETH`), or contract
address (`0x...`). Unknown symbols may return candidate matches from
CoinGecko/DexScreener instead of hard failing.

### `token contract`

Expand All @@ -303,6 +324,74 @@ chainpilot [--chain-id <N>] token contract <TOKEN>

On-chain contract details: proxy, owner, implementation address.

### `token price`

```bash
chainpilot [--chain-id <N>] token price <TOKEN>
```

Real-time price, short-term and mid-term changes, and 24h high/low. CoinGecko is the primary data
source; DexScreener is a fallback for `price`, `price_change_1h`, and `price_change_24h` when
CoinGecko has no value (e.g. long-tail tokens not listed there).

| Field | Primary | Fallback | Notes |
|---|---|---|---|
| `price` | CoinGecko | DexScreener | USD spot price |
| `price_change_1h` | CoinGecko | DexScreener | % change over 1 hour |
| `price_change_24h` | CoinGecko | DexScreener | % change over 24 hours |
| `price_change_7d` | CoinGecko | — | % change over 7 days |
| `high_24h` | CoinGecko | — | 24h high (USD) |
| `low_24h` | CoinGecko | — | 24h low (USD) |

The JSON output includes a `sources` map indicating which API supplied each field, so callers can
distinguish CoinGecko-backed values from DexScreener-backed ones.

### `token liquidity`

```bash
chainpilot [--chain-id <N>] token liquidity <TOKEN>
```

Liquidity overview from DexScreener: top liquidity across all pairs, pair count,
and top pair details (DEX, pair address, 24h volume).

| Field | Source | Notes |
|---|---|---|
| `top_liquidity` | DexScreener | Highest single-pair liquidity (USD) |
| `pair_count` | DexScreener | Number of matching base-token pairs |
| `top_pair.dex` | DexScreener | DEX ID of the top pair (e.g. `uniswap`) |
| `top_pair.pair_address` | DexScreener | Contract address of the top pair |
| `top_pair.liquidity` | DexScreener | Top pair's liquidity (USD) |
| `top_pair.volume_24h` | DexScreener | Top pair's 24h trading volume (USD) |

### `token risk`

```bash
chainpilot [--chain-id <N>] token risk <TOKEN>
```

Token risk analysis from GoPlus Security: honeypot detection, blacklist status,
transfer restrictions, minting, owner privileges, and buy/sell tax. Free API,
no credentials required.

| Field | Source | Notes |
|---|---|---|
| `risk_level` | GoPlus (derived) | `low`, `medium`, or `high` |
| `risk_score` | GoPlus (derived) | 0–100 composite score |
| `honeypot` | GoPlus | Whether the token is a honeypot |
| `blacklist` | GoPlus | Whether the token has a blacklist |
| `transfer_restricted` | GoPlus | Whether transfers are pausable |
| `mintable` | GoPlus | Whether new tokens can be minted |
| `owner_privileged` | GoPlus | Whether owner can change balances |
| `tax_buy` | GoPlus | Buy tax percentage |
| `tax_sell` | GoPlus | Sell tax percentage |

Native tokens (ETH, BNB, etc.) return hardcoded low-risk defaults.

Use `token risk` for the current token-specific GoPlus implementation. The
older top-level `risk token` command remains available but may expose a simpler
legacy risk shape.

### `token add`

```bash
Expand Down Expand Up @@ -416,13 +505,17 @@ Single approval state.

## Token Resolution

Tokens can be a symbol or a `0x` address. Resolution order:
Tokens can be a symbol, native token symbol, or a `0x` address. Resolution order:

1. Native token symbol (`ETH`, `BNB`, etc.)
2. Raw `0x` address — decimals fetched on-chain
3. DODO tokenlist cache (1-hour TTL)
4. Local custom token store (`token add` and successful address-based quotes)

For `token info`, `token price`, `token liquidity`, and `token risk`, unresolved
symbols can return external-source candidates. Treat those as suggestions only;
confirm with the user before using any candidate address in a swap or approval.

---

## Supported Chains
Expand Down Expand Up @@ -451,6 +544,63 @@ For unsupported chain IDs, pass `--rpc-url` manually.

---

## `config` Subcommands

Manage API keys and configuration values. Settings are persisted in a config file
(`~/.local/share/chain/config.env` on Linux). Existing environment variables and
`.env` values take precedence over this persisted file at runtime.

### `config set`

```bash
chainpilot config set <KEY> <VALUE>
```

Save an API key or configuration value. The value is written to the persistent config file
and immediately available to the current process. On Unix, the config file is
written with owner-only permissions (`0600`).

### `config get`

```bash
chainpilot config get <KEY>
```

Show the current value of a configuration key. Sensitive values are partially masked.

### `config list`

```bash
chainpilot config list
```

Show all configurable keys with their current values (sensitive values masked).

### `config unset`

```bash
chainpilot config unset <KEY>
```

Remove a configuration key from the config file.

### Configurable Keys

| Key | Env Var | Sensitive | Description |
|---|---|---|---|
| `dodo_api_key` | `DODO_API_KEY` | Yes | DODO API key for swap routing |
| `dodo_project_id` | `DODO_PROJECT_ID` | No | DODO project ID for tokenlist API |
| `coingecko_api_key` | `COINGECKO_API_KEY` | Yes | CoinGecko API key for price data |

Only these keys are supported by `chainpilot config` today. Other runtime
settings, such as `COINGECKO_API_URL` and `DEXSCREENER_API_URL`, can still be
provided via environment variables or `.env`.

**Runtime config precedence**: CLI flag > existing environment variable / `.env`
file > `config.env` file > compile-time default.

---

## Scripting Patterns

```bash
Expand Down
6 changes: 5 additions & 1 deletion src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
mod dodo;
mod token_metadata;

pub use dodo::DodoClient;
pub use token_metadata::TokenMetadataClient;

use crate::config::AppConfig;
use crate::error::Result;
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, USER_AGENT};

pub struct ApiClients {
pub dodo: DodoClient,
pub token_metadata: TokenMetadataClient,
}

impl ApiClients {
Expand All @@ -31,11 +34,12 @@ impl ApiClients {

Ok(Self {
dodo: DodoClient::new(
client,
client.clone(),
&config.dodo_api_url,
&config.dodo_api_key,
&config.dodo_project_id,
),
token_metadata: TokenMetadataClient::new(client, config),
})
}
}
Loading
Loading