Skip to content

Commit 0b29989

Browse files
Michael YuanMichael Yuan
authored andcommitted
feat: deposit/withdraw via HyperUnit bridge + bridge-status
- fintool deposit <amount> ETH/BTC/SOL — generates Unit deposit address on native chain - fintool withdraw <amount> ETH/BTC/SOL --to <addr> — generates Unit withdrawal address on HL - fintool bridge-status — shows all Unit operations for the wallet - USDC deposits/withdrawals route to HL's native Arbitrum bridge with instructions - Fee estimation from Unit API - Minimum amount validation per asset
1 parent e8535df commit 0b29989

File tree

7 files changed

+723
-0
lines changed

7 files changed

+723
-0
lines changed

src/cli.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,28 @@ pub enum Commands {
6060
/// Get stock reports (10-K annual, 10-Q quarterly) from SEC EDGAR
6161
#[command(subcommand)]
6262
Report(ReportCmd),
63+
64+
/// Deposit assets to Hyperliquid via Unit bridge (ETH/BTC/SOL) or Arbitrum (USDC)
65+
Deposit {
66+
/// Amount to deposit (e.g. 0.5)
67+
amount: String,
68+
/// Asset: ETH, BTC, SOL, or USDC
69+
asset: String,
70+
},
71+
72+
/// Withdraw assets from Hyperliquid via Unit bridge (ETH/BTC/SOL) or Arbitrum (USDC)
73+
Withdraw {
74+
/// Amount to withdraw (e.g. 0.5)
75+
amount: String,
76+
/// Asset: ETH, BTC, SOL, or USDC
77+
asset: String,
78+
/// Destination address on the native chain (required for BTC, optional for ETH/SOL)
79+
#[arg(long)]
80+
to: Option<String>,
81+
},
82+
83+
/// Show bridge operation status (deposits/withdrawals via Unit)
84+
BridgeStatus,
6385
}
6486

6587
#[derive(Subcommand)]

src/commands/bridge_status.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//! `fintool bridge-status` — show all Unit bridge operations for the configured wallet
2+
3+
use anyhow::Result;
4+
use colored::Colorize;
5+
use serde_json::json;
6+
7+
use crate::config;
8+
use crate::unit;
9+
10+
pub async fn run(json_out: bool) -> Result<()> {
11+
let cfg = config::load_hl_config()?;
12+
let ops = unit::get_operations(&cfg.address, cfg.testnet).await?;
13+
14+
if json_out {
15+
let out = json!({
16+
"address": cfg.address,
17+
"addresses": ops.addresses,
18+
"operations": ops.operations.iter().map(|op| json!({
19+
"id": op.operation_id,
20+
"created_at": op.op_created_at,
21+
"asset": op.asset,
22+
"source_chain": op.source_chain,
23+
"destination_chain": op.destination_chain,
24+
"amount": unit::format_amount(&op.source_amount, &op.asset),
25+
"amount_raw": op.source_amount,
26+
"state": op.state,
27+
"source_tx": op.source_tx_hash,
28+
"destination_tx": op.destination_tx_hash,
29+
"confirmations": op.source_tx_confirmations,
30+
})).collect::<Vec<_>>(),
31+
});
32+
println!("{}", serde_json::to_string_pretty(&out)?);
33+
} else {
34+
println!("{}", "━".repeat(60).dimmed());
35+
println!(
36+
" {} for {}",
37+
"Bridge Operations".cyan().bold(),
38+
cfg.address.dimmed()
39+
);
40+
println!("{}", "━".repeat(60).dimmed());
41+
42+
if ops.operations.is_empty() {
43+
println!();
44+
println!(" No bridge operations found.");
45+
println!();
46+
return Ok(());
47+
}
48+
49+
for op in &ops.operations {
50+
let direction = if op.destination_chain == "hyperliquid" {
51+
"DEPOSIT".green()
52+
} else {
53+
"WITHDRAW".red()
54+
};
55+
let amount = unit::format_amount(&op.source_amount, &op.asset);
56+
let state_color = match op.state.as_str() {
57+
"done" => op.state.green(),
58+
s if s.contains("wait") || s.contains("Wait") => op.state.yellow(),
59+
_ => op.state.normal(),
60+
};
61+
62+
println!();
63+
println!(
64+
" {} {} {} → {}",
65+
direction,
66+
amount.cyan(),
67+
op.source_chain,
68+
op.destination_chain
69+
);
70+
println!(
71+
" {} {} {} {}",
72+
"State:".dimmed(),
73+
state_color,
74+
"Created:".dimmed(),
75+
op.op_created_at.dimmed()
76+
);
77+
if !op.source_tx_hash.is_empty() {
78+
let hash = if op.source_tx_hash.len() > 20 {
79+
format!("{}…", &op.source_tx_hash[..20])
80+
} else {
81+
op.source_tx_hash.clone()
82+
};
83+
println!(" {} {}", "Src TX: ".dimmed(), hash);
84+
}
85+
if let Some(ref dtx) = op.destination_tx_hash {
86+
if !dtx.is_empty() {
87+
let hash = if dtx.len() > 20 {
88+
format!("{}…", &dtx[..20])
89+
} else {
90+
dtx.clone()
91+
};
92+
println!(" {} {}", "Dst TX: ".dimmed(), hash);
93+
}
94+
}
95+
}
96+
println!();
97+
}
98+
99+
Ok(())
100+
}

src/commands/deposit.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
//! `fintool deposit <amount> <asset>` — generate a Unit deposit address and show instructions
2+
//! For ETH/BTC/SOL: uses Unit bridge (native chain → Hyperliquid)
3+
//! For USDC: uses Hyperliquid's Arbitrum bridge (HL SDK)
4+
5+
use anyhow::{bail, Result};
6+
use colored::Colorize;
7+
use serde_json::json;
8+
9+
use crate::config;
10+
use crate::unit;
11+
12+
pub async fn run(amount: &str, asset: &str, json_out: bool) -> Result<()> {
13+
let asset_lower = asset.to_lowercase();
14+
15+
if asset_lower == "usdc" {
16+
return deposit_usdc(amount, json_out).await;
17+
}
18+
19+
if !unit::is_supported(&asset_lower) {
20+
bail!(
21+
"Unsupported asset '{}'. Supported: ETH, BTC, SOL, USDC",
22+
asset
23+
);
24+
}
25+
26+
let cfg = config::load_hl_config()?;
27+
let chain = unit::native_chain(&asset_lower).unwrap();
28+
let min = unit::minimum_amount(&asset_lower).unwrap_or("unknown");
29+
30+
// Generate deposit address: native chain → hyperliquid
31+
let resp =
32+
unit::generate_address(chain, "hyperliquid", &asset_lower, &cfg.address, cfg.testnet)
33+
.await?;
34+
35+
// Estimate fees
36+
let fees = unit::estimate_fees(cfg.testnet).await.ok();
37+
38+
if json_out {
39+
let mut out = json!({
40+
"action": "deposit",
41+
"asset": asset.to_uppercase(),
42+
"amount": amount,
43+
"source_chain": chain,
44+
"destination": "hyperliquid",
45+
"hl_address": cfg.address,
46+
"deposit_address": resp.address,
47+
"minimum": min,
48+
"instructions": format!(
49+
"Send {} {} on {} to {}",
50+
amount,
51+
asset.to_uppercase(),
52+
chain,
53+
resp.address
54+
),
55+
});
56+
if let Some(ref f) = fees {
57+
if let Some(chain_fees) = f.get(chain) {
58+
out["estimated_fees"] = chain_fees.clone();
59+
}
60+
}
61+
println!("{}", serde_json::to_string_pretty(&out)?);
62+
} else {
63+
println!("{}", "━".repeat(50).dimmed());
64+
println!(
65+
" {} {} {} → Hyperliquid",
66+
"Deposit".green().bold(),
67+
amount,
68+
asset.to_uppercase().cyan()
69+
);
70+
println!("{}", "━".repeat(50).dimmed());
71+
println!();
72+
println!(
73+
" {} {}",
74+
"Source chain:".dimmed(),
75+
chain.yellow()
76+
);
77+
println!(
78+
" {} {}",
79+
"HL address: ".dimmed(),
80+
cfg.address.cyan()
81+
);
82+
println!(
83+
" {} {}",
84+
"Minimum: ".dimmed(),
85+
min
86+
);
87+
println!();
88+
println!(
89+
" {} Send {} {} on {} to:",
90+
"→".green().bold(),
91+
amount,
92+
asset.to_uppercase(),
93+
chain
94+
);
95+
println!();
96+
println!(" {}", resp.address.green().bold());
97+
println!();
98+
if let Some(ref f) = fees {
99+
let key_deposit = format!("{}-depositEta", chain);
100+
let key_fee = format!("{}-depositFee", chain);
101+
if let Some(eta) = f.get(chain).and_then(|c| c.get(&key_deposit)) {
102+
println!(
103+
" {} ~{}",
104+
"Est. time: ".dimmed(),
105+
eta.as_str().unwrap_or("unknown")
106+
);
107+
}
108+
if let Some(fee) = f.get(chain).and_then(|c| c.get(&key_fee)) {
109+
let fee_str = unit::format_amount(
110+
&fee.as_f64().unwrap_or(0.0).to_string(),
111+
&asset_lower,
112+
);
113+
println!(
114+
" {} {}",
115+
"Est. fee: ".dimmed(),
116+
fee_str
117+
);
118+
}
119+
}
120+
println!();
121+
println!(
122+
" {} This address is permanent for your HL wallet.",
123+
"ℹ".blue()
124+
);
125+
println!(
126+
" {} Track status: fintool bridge-status",
127+
"ℹ".blue()
128+
);
129+
println!();
130+
}
131+
132+
Ok(())
133+
}
134+
135+
async fn deposit_usdc(amount: &str, json_out: bool) -> Result<()> {
136+
let cfg = config::load_hl_config()?;
137+
138+
if json_out {
139+
let out = json!({
140+
"action": "deposit",
141+
"asset": "USDC",
142+
"amount": amount,
143+
"source_chain": "arbitrum",
144+
"destination": "hyperliquid",
145+
"hl_address": cfg.address,
146+
"instructions": format!(
147+
"Send {} USDC on Arbitrum to the Hyperliquid bridge contract. \
148+
Use the Hyperliquid web UI at https://app.hyperliquid.xyz or \
149+
the HL SDK's deposit method.",
150+
amount),
151+
"note": "USDC deposits use Hyperliquid's native Arbitrum bridge, not Unit.",
152+
});
153+
println!("{}", serde_json::to_string_pretty(&out)?);
154+
} else {
155+
println!("{}", "━".repeat(50).dimmed());
156+
println!(
157+
" {} {} {} → Hyperliquid",
158+
"Deposit".green().bold(),
159+
amount,
160+
"USDC".cyan()
161+
);
162+
println!("{}", "━".repeat(50).dimmed());
163+
println!();
164+
println!(
165+
" {} Arbitrum (native HL bridge, not Unit)",
166+
"Source chain:".dimmed()
167+
);
168+
println!(
169+
" {} {}",
170+
"HL address: ".dimmed(),
171+
cfg.address.cyan()
172+
);
173+
println!();
174+
println!(
175+
" {} USDC deposits go through Hyperliquid's native Arbitrum bridge.",
176+
"ℹ".blue()
177+
);
178+
println!(
179+
" {} Use https://app.hyperliquid.xyz → Deposit",
180+
"→".green().bold()
181+
);
182+
println!(
183+
" {} Or send USDC on Arbitrum to the HL bridge contract.",
184+
"→".green().bold()
185+
);
186+
println!();
187+
}
188+
189+
Ok(())
190+
}

src/commands/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub mod balance;
2+
pub mod bridge_status;
23
pub mod cancel;
4+
pub mod deposit;
35
pub mod news;
46
pub mod options;
57
pub mod order;
@@ -9,3 +11,4 @@ pub mod positions;
911
pub mod predict;
1012
pub mod quote;
1113
pub mod report;
14+
pub mod withdraw;

0 commit comments

Comments
 (0)