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
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ bs58 = "0.5"
bincode = "1"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4"] }
sha2 = "0.10"

# OS keyring for wallet secrets.
# Linux uses the pure-Rust Secret Service backend (zbus + RustCrypto) rather
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ cargo install --path .
## Features

- **4 APIs**: EVM Swap (Allowance Holder), Gasless Swap, Solana Swap, Cross-Chain
- **20 chains**: Ethereum, Base, Arbitrum, Optimism, Polygon, BSC, Avalanche, Linea, Scroll, Blast, Mantle, Berachain, Sonic, Unichain, World Chain, Abstract, Ink, Monad, HyperEVM, Solana
- **21 chains**: Ethereum, Base, Arbitrum, Optimism, Polygon, BSC, Avalanche, Linea, Scroll, Blast, Mantle, Berachain, Sonic, Unichain, World Chain, Abstract, Ink, Monad, HyperEVM, Solana, Tron
- **Agent-first**: Auto-detect non-TTY for JSON output, structured error codes, stable exit codes, inline `RESPONSE:` schemas in every `--help`
- **Safe by default**: OS keyring for wallet secrets, transaction simulation before every execution, `--dry-run` mode, exact token approvals
- **Rich UX**: Colored tables, progress spinners, interactive confirmation, shell completions
Expand Down Expand Up @@ -118,6 +118,9 @@ By default, `wallet.evm` and `wallet.solana` (when given key material rather tha
| `0x config set wallet.solana /path/to/file.json` | `~/.0x-config/config.toml` (it's a path) |
| `0x config set wallet.solana <base58>` | OS keyring |
| `ZEROX_EVM_PRIVATE_KEY` / `ZEROX_SOLANA_KEYPAIR` env var | Read directly, never persisted |
| `0x config set wallet.tron <hex-key>` | OS keyring |
| `0x config set wallet.tron <hex-key> --plaintext` | `~/.0x-config/config.toml` |
| `ZEROX_TRON_PRIVATE_KEY` env var | Read directly, never persisted |

`0x config show` reports keyring-stored wallets as `<stored in keyring>`. If the OS keyring is unavailable (e.g. headless Linux with no DBus), use `--plaintext` or the env vars.

Expand All @@ -130,6 +133,7 @@ Environment variables always take precedence over config file values.
| `ZEROX_API_KEY` | 0x API key |
| `ZEROX_EVM_PRIVATE_KEY` | EVM private key (hex) |
| `ZEROX_SOLANA_KEYPAIR` | Solana keypair file path or base58 |
| `ZEROX_TRON_PRIVATE_KEY` | Tron private key (hex) |
| `ZEROX_DEFAULT_CHAIN` | Default chain name or ID |
| `ZEROX_RPC_URL` | Override RPC URL for any chain |
| `ZEROX_TELEMETRY` | Set falsy (`0`/`false`/`off`) to disable usage telemetry |
Expand Down Expand Up @@ -233,6 +237,8 @@ No gas fees required. The 0x protocol handles gas on your behalf.

### Cross-Chain Swap

> **Note:** Tron is supported for bridging only — it is not available in `swap`, `price`, or `gasless`. Use `--from tron` or `--to tron` with `cross-chain`.

```bash
# Interactive (shows quote table, lets you pick)
0x cross-chain \
Expand Down Expand Up @@ -440,14 +446,15 @@ Every interactive prompt has a flag equivalent:
| 81457 | blast | Blast | ETH |
| 534352 | scroll | Scroll | ETH |
| solana | solana | Solana | SOL |
| tron | tron | Tron | TRX |

## Security

- **OS keyring by default**: Wallet secrets (`wallet.evm`, `wallet.solana` key material) are stored in the OS keyring — macOS Keychain, Linux libsecret, Windows Credential Locker. Use `--plaintext` to opt out only when the keyring isn't available.
- **Config file**: Created with `0600` permissions (owner read/write only)
- **Config directory**: Created with `0700` permissions
- **Redaction**: `0x config show` and `0x config get` never reveal secret material. Wallets stored in the keyring show as `<stored in keyring>`; plaintext wallets show as `***redacted***`; Solana file paths show verbatim because the path itself isn't sensitive.
- **Transaction simulation**: Every transaction is simulated via `eth_call` (EVM) or `simulate_transaction` (Solana) before submission
- **Transaction simulation**: EVM and Solana transactions are simulated via `eth_call` or `simulate_transaction` before submission. Tron cross-chain transactions are not pre-simulated.
- **Approval strategy**: Default is `exact` (only approve the needed amount). Use `--approval unlimited` for max approval.
- **Environment variables**: Sensitive values like private keys can be set via env vars (`ZEROX_EVM_PRIVATE_KEY`, `ZEROX_SOLANA_KEYPAIR`) to avoid persisting them at all — read-once, never written to disk or keyring.

Expand Down
3 changes: 3 additions & 0 deletions skills/0x-trade/references/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Non-interactive (agent-driven) setup:
| `rpc.<chain>` | Custom RPC URL per chain, e.g. `rpc.base` |
| `wallet.evm` | EVM private key (hex) — secret → keyring |
| `wallet.solana` | Keypair file path (→ config) or base58/JSON-array secret (→ keyring) |
| `wallet.tron` | Tron private key (hex) — secret → keyring |
| `rpc.tron` | Custom Tron full-node RPC URL (default: `https://api.trongrid.io`) |
| `active_profile` | Profile applied when --profile isn't passed |
| `profiles.<name>.base_url` | Override the API base URL for a profile |
| `profiles.<name>.api_key` | API key for a profile |
Expand All @@ -46,6 +48,7 @@ Non-interactive (agent-driven) setup:
| `ZEROX_API_KEY` | `api_key` |
| `ZEROX_EVM_PRIVATE_KEY` | `wallet.evm` |
| `ZEROX_SOLANA_KEYPAIR` | `wallet.solana` (path or base58) |
| `ZEROX_TRON_PRIVATE_KEY` | `wallet.tron` |
| `ZEROX_DEFAULT_CHAIN` | `defaults.chain` |
| `ZEROX_RPC_URL` | RPC for the current command |
| `ZEROX_OUTPUT` | `-o/--output` format |
Expand Down
4 changes: 3 additions & 1 deletion skills/0x-trade/references/cross-chain.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Cross-chain swaps (bridging)

Swap a token on one chain for a token on another in a single command. Supports EVM↔EVM, EVM↔Solana. The CLI fetches multiple bridge quotes, executes the origin-chain transaction, and can track the bridge until funds land on the destination.
Swap a token on one chain for a token on another in a single command. Supports EVM↔EVM, EVM↔Solana, and EVM↔Tron. The CLI fetches multiple bridge quotes, executes the origin-chain transaction, and can track the bridge until funds land on the destination.

**Tron:** Tron addresses are base58check (`T…`). A Tron wallet (`wallet.tron` / `ZEROX_TRON_PRIVATE_KEY`) is required to bridge from or to Tron. Tron is bridging-only — it is not available in `swap`, `price`, or `gasless`.

## Quote + execute

Expand Down
5 changes: 4 additions & 1 deletion src/api/cross_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub struct CrossChainStep {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CrossChainTransaction {
pub chain_type: String, // "evm" or "svm"
pub chain_type: String, // "evm", "svm", or "tvm"
pub details: CrossChainTxDetails,
}

Expand All @@ -70,6 +70,9 @@ pub struct CrossChainTxDetails {

// SVM fields
pub serialized_transaction: Option<String>,

// TVM (Tron) field
pub owner_address: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
90 changes: 88 additions & 2 deletions src/chain/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod evm;
pub mod retry;
pub mod solana;
pub mod tron;

use crate::error::CliError;
use crate::output::human::DataTable;
Expand Down Expand Up @@ -30,6 +31,7 @@ pub struct ChainInfo {
pub enum ChainType {
Evm,
Svm,
Tvm,
}

/// Chain identifier that supports both numeric IDs and the special "solana"
Expand All @@ -42,13 +44,15 @@ pub enum ChainType {
pub enum ChainId {
Numeric(u64),
Solana,
Tron,
}

impl std::fmt::Display for ChainId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChainId::Numeric(id) => write!(f, "{id}"),
ChainId::Solana => write!(f, "solana"),
ChainId::Tron => write!(f, "tron"),
}
}
}
Expand All @@ -58,6 +62,7 @@ impl Serialize for ChainId {
match self {
ChainId::Numeric(id) => serializer.serialize_u64(*id),
ChainId::Solana => serializer.serialize_str("solana"),
ChainId::Tron => serializer.serialize_str("tron"),
}
}
}
Expand All @@ -71,15 +76,24 @@ impl ChainInfo {
self.chain_type == ChainType::Evm
}

pub fn is_tron(&self) -> bool {
self.chain_type == ChainType::Tvm
}

pub fn explorer_tx_url(&self, tx_hash: &str) -> String {
format!("{}/tx/{}", self.explorer_url, tx_hash)
if self.chain_type == ChainType::Tvm {
format!("{}/#/transaction/{}", self.explorer_url, tx_hash)
} else {
format!("{}/tx/{}", self.explorer_url, tx_hash)
}
}

/// Get the numeric chain ID (for API calls). Returns None for Solana.
pub fn numeric_id(&self) -> Option<u64> {
match self.id {
ChainId::Numeric(id) => Some(id),
ChainId::Solana => None,
ChainId::Tron => None,
}
}

Expand All @@ -99,12 +113,30 @@ impl ChainInfo {
}

/// Get the chain identifier for 0x API calls.
/// For EVM: numeric string. For Solana: "solana".
/// For EVM: numeric string. For Solana: "solana". For Tron: "tron".
pub fn api_chain_id(&self) -> String {
match self.id {
ChainId::Numeric(id) => id.to_string(),
ChainId::Solana => "solana".to_string(),
ChainId::Tron => "tron".to_string(),
}
}

/// Error returned when a Tron chain is used on a command that only the
/// cross-chain API supports (swap/price/gasless).
pub fn reject_if_tron(&self, command: &str) -> Result<(), CliError> {
if self.is_tron() {
return Err(CliError::Api {
code: crate::error::ErrorCode::InputInvalid,
message: format!("Tron is not supported by '{command}'"),
status: None,
details: None,
suggestion: Some(
"Tron is supported for bridging only — use '0x cross-chain --from/--to tron'".into(),
),
});
}
Ok(())
}
}

Expand Down Expand Up @@ -292,6 +324,15 @@ const CHAINS: &[ChainInfo] = &[
chain_type: ChainType::Svm,
default_rpc_url: Some("https://api.mainnet-beta.solana.com"),
},
ChainInfo {
id: ChainId::Tron,
name: "tron",
display_name: "Tron",
native_token: "TRX",
explorer_url: "https://tronscan.org",
chain_type: ChainType::Tvm,
default_rpc_url: Some("https://api.trongrid.io"),
},
];

/// Read-only view of every supported chain. Consumers iterate this for
Expand Down Expand Up @@ -362,6 +403,16 @@ pub fn validate_token_address(token: &str, chain_info: &ChainInfo) -> Result<(),
),
});
}
} else if chain_info.is_tron() && !crate::chain::tron::is_valid_tron_address(token) {
return Err(CliError::Api {
code: crate::error::ErrorCode::InputInvalid,
message: format!("'{token}' is not a valid Tron token address"),
status: None,
details: None,
suggestion: Some(
"Use the base58check TRC20 address, e.g. TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t for USDT on Tron".into(),
),
});
}
Ok(())
}
Expand Down Expand Up @@ -527,4 +578,39 @@ mod tests {
"\"solana\""
);
}

#[test]
fn test_resolve_tron() {
let chain = resolve_chain("tron").unwrap();
assert!(chain.is_tron());
assert!(!chain.is_evm());
assert!(!chain.is_solana());
assert_eq!(chain.numeric_id(), None);
assert_eq!(chain.api_chain_id(), "tron");
}

#[test]
fn test_chain_id_tron_serializes_as_string() {
assert_eq!(
serde_json::to_string(&ChainId::Tron).unwrap(),
"\"tron\""
);
}

#[test]
fn test_tron_explorer_tx_url() {
let chain = resolve_chain("tron").unwrap();
assert_eq!(
chain.explorer_tx_url("abc123"),
"https://tronscan.org/#/transaction/abc123"
);
}

#[test]
fn test_validate_tron_token_address() {
let tron = resolve_chain("tron").unwrap();
assert!(validate_token_address("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", tron).is_ok());
assert!(validate_token_address("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", tron).is_err());
assert!(validate_token_address("not-an-address", tron).is_err());
}
}
Loading
Loading