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
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 = "1.2.0"
version = "1.2.1"
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ They include the legacy preview fields plus a stable external-signer payload:
`source`, `operation`, `chain_id`, `caip2`, `from`,
`transaction { to, value, data, chain_id }`, optional `quote`, and `risk`
metadata. `operation` is one of `swap_execute`, `approve`, or `revoke`.
For swap dry-runs, `risk.spender` is populated only for ERC-20 inputs and
points to the chain's configured DODOApprove contract; native-token inputs do
not have an allowance spender. `risk.token_in.amount_usd` is included only when
the input or output token address matches ChainPilot's trusted USD-stablecoin
registry; symbols alone are not trusted for budget metadata. Swap minimum-output
metadata reports `min_amount_raw` in token base units and `min_amount_display`
in human-readable units.
For swaps, pass the dry-run wallet with `--wallet`; if omitted, execute dry-run
falls back to the global `--wallet-address` / `WALLET_ADDRESS` sender context.
For approve/revoke, pass the sender with the global `--wallet-address`.
Expand Down
9 changes: 7 additions & 2 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,13 @@ JSON 模式下,swap / approve / revoke 的 dry-run 不会签名或广播交易
响应会保留原有预览字段,并额外包含稳定的外部 signer payload:
`source`、`operation`、`chain_id`、`caip2`、`from`、
`transaction { to, value, data, chain_id }`、可选 `quote` 和 `risk` metadata。
`operation` 取值为 `swap_execute`、`approve` 或 `revoke`。兑换执行优先使用
`--wallet` 传入 dry-run 钱包;如果省略,会 fallback 到全局
`operation` 取值为 `swap_execute`、`approve` 或 `revoke`。swap dry-run 中,
`risk.spender` 只会在输入资产为 ERC-20 时写入,并指向该链
配置的 DODOApprove 合约;原生资产输入没有 allowance spender。
`risk.token_in.amount_usd` 只会在输入或输出 token 地址命中 ChainPilot 可信
USD 稳定币地址表时写入,不依赖 token symbol。兑换最低收到 metadata 中,
`min_amount_raw` 使用 token base units,`min_amount_display` 使用人类可读单位。
兑换执行优先使用 `--wallet` 传入 dry-run 钱包;如果省略,会 fallback 到全局
`--wallet-address/WALLET_ADDRESS` sender context。approve / revoke 使用全局
`--wallet-address`。approve / revoke dry-run 会在 `transaction.data` 中返回
ERC-20 `approve(address,uint256)` calldata。Privy 等外部 signer 系统仍需要在
Expand Down
12 changes: 10 additions & 2 deletions skills/chainpilot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ first or use the matching `*_display` field instead.
| `from_amount` | ✓ | Same value as `from_amount_display`, stringified |
| `to_amount_display` | ✓ | Always prefer this for display |
| `to_amount` | ✓ | Same value as `to_amount_display`, stringified |
| `to_amount_min` | **✗ raw** | Integer in `to_token.decimals` units — divide by `10^to_token.decimals` to display |
| `to_amount_min` | ✓ | Minimum output, already human-readable |
| `value` | **✗ raw** | Native token value in wei — divide by `1e18` to show as ETH |
| `exchange_rate` | ✓ | Human-readable ratio |
| `price_impact_pct` | ✓ | Already a percentage |
Expand All @@ -176,7 +176,7 @@ first or use the matching `*_display` field instead.
| Field | Display-ready? | Notes |
|---|---|---|
| `expected_out` | ✓ | Mirrors `to_amount` — human-readable |
| `min_out` | **✗ raw** | Same raw integer as `to_amount_min`; divide by `10^to_token.decimals` |
| `min_out` | ✓ | Mirrors `to_amount_min` — already human-readable |
| `wallet_balance` | **✗ raw** | Integer in from-token's smallest unit |
| `current_allowance` | **✗ raw** | Integer in from-token's smallest unit |
| `suggested_approve_amount` | **✗ raw** | Integer in from-token's smallest unit |
Expand Down Expand Up @@ -259,6 +259,14 @@ The JSON `data` object includes the legacy preview fields plus:
| `quote` | Present for quote-derived swap execution |
| `risk` | Token, amount, spender/router, and gas metadata when available |

For swap dry-runs, `risk.spender` is present only when the input asset is an
ERC-20 and it points to the chain's configured DODOApprove contract. Native-token
swaps do not have an allowance spender. `risk.token_in.amount_usd` is emitted
only when the input or output token address matches ChainPilot's trusted
USD-stablecoin registry; do not infer USD budget metadata from token symbols.
Minimum-output metadata uses `min_amount_raw` for token base units and
`min_amount_display` for human-readable units.

All external-signer dry-run commands require a sender wallet. For
`swap execute --dry-run`, use `--wallet`; if it is omitted, the command falls
back to the global `--wallet-address` / `WALLET_ADDRESS` sender context. For
Expand Down
44 changes: 42 additions & 2 deletions src/api/dodo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ impl DodoClient {
let expires_at = now + Duration::from_secs(quote_ttl_secs);
let route_summary = parse_route_summary(&data.route_info);

let target_decimals = data.target_decimals.unwrap_or(to_token.decimals);

Ok(Quote {
quote_id: Uuid::new_v4(),
created_at: now,
Expand All @@ -406,14 +408,14 @@ impl DodoClient {
.target_symbol
.unwrap_or_else(|| to_token.symbol.clone()),
address: to_token.address.clone(),
decimals: data.target_decimals.unwrap_or(to_token.decimals),
decimals: target_decimals,
chain_id: req.chain_id,
},
from_amount: req.amount.clone(),
from_amount_display: req.amount_display,
to_amount: data.res_amount.to_string(),
to_amount_display: data.res_amount,
to_amount_min: data.min_return_amount.clone(),
to_amount_min: raw_amount_to_display(&data.min_return_amount, target_decimals),
price_impact_pct: data.price_impact,
exchange_rate: if req.amount_display > 0.0 {
data.res_amount / req.amount_display
Expand Down Expand Up @@ -445,6 +447,30 @@ fn parse_optional_u64(value: Option<FlexibleU64>) -> Result<Option<u64>> {
value.map(FlexibleU64::into_u64).transpose()
}

fn raw_amount_to_display(raw: &str, decimals: u8) -> String {
let raw = raw.trim();
if raw.is_empty() || raw.starts_with('-') {
return raw.to_string();
}
let decimals = decimals as usize;
if decimals == 0 {
return raw.to_string();
}
let padded = if raw.len() <= decimals {
format!("{}{}", "0".repeat(decimals + 1 - raw.len()), raw)
} else {
raw.to_string()
};
let split = padded.len().saturating_sub(decimals);
let (whole, frac) = padded.split_at(split);
let frac = frac.trim_end_matches('0');
if frac.is_empty() {
whole.to_string()
} else {
format!("{whole}.{frac}")
}
}

fn parse_route_summary(route_info: &Option<DodoRouteInfo>) -> Vec<RouteHop> {
let Some(route_info) = route_info else {
return Vec::new();
Expand Down Expand Up @@ -478,3 +504,17 @@ fn parse_route_summary(route_info: &Option<DodoRouteInfo>) -> Vec<RouteHop> {

hops
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn raw_amount_to_display_converts_dodo_min_return_once() {
assert_eq!(
raw_amount_to_display("1679119161479651", 18),
"0.001679119161479651"
);
assert_eq!(raw_amount_to_display("2970000000", 6), "2970");
}
}
Loading
Loading