use clap::Parser;
use fuels::{
accounts::{
provider::Provider,
signers::private_key::PrivateKeySigner,
wallet::Wallet,
},
crypto::SecretKey,
};
use o2_deploy::{
DeployParams,
MarketsConfigPartial,
load_config_from_file,
};
use std::str::FromStr;
#[derive(Parser)]
#[command(name = "o2-deploy", about = "Deploy O2 exchange contracts")]
struct Cli {
#[arg(long, env = "DEPLOY_KEY")]
deploy_key: String,
#[arg(
long = "fuel-rpc",
env = "FUEL_RPC",
default_value = "http://127.0.0.1:4000"
)]
fuel_rpc: url::Url,
#[arg(long, env = "DEPLOY_CONFIG", default_value = "./deploy_config.json")]
deploy_config: String,
#[arg(long, env = "OUTPUT_FILE")]
output: Option<String>,
#[arg(long, env = "DEPLOY_WHITELIST", default_value = "false")]
deploy_whitelist: bool,
#[arg(long, env = "DEPLOY_BLACKLIST", default_value = "true")]
deploy_blacklist: bool,
#[arg(long, env, default_value = "false")]
upgrade_bytecode: bool,
#[arg(long, env = "DEPLOY_NEW_PROXY_OWNER")]
new_proxy_owner: Option<String>,
#[arg(long, env = "DEPLOY_NEW_CONTRACT_OWNER")]
new_contract_owner: Option<String>,
#[arg(long, env = "DEPLOY_TRIAL_COSIGNER")]
trial_cosigner: Option<String>,
#[arg(long, env = "DEPLOY_MARGIN_TIER_ONLY")]
margin_tier_only: bool,
#[arg(long, env = "DEPLOY_MARGIN_COSIGNER")]
margin_cosigner: Option<String>,
#[arg(long, env = "DEPLOY_MARGIN_LIQUIDATOR")]
margin_liquidator: Option<String>,
#[arg(long, env = "DEPLOY_TRIAL_CREATOR")]
trial_creator: Option<String>,
#[arg(
long,
env = "DEPLOY_REVOKE_ORDERBOOK_MAINTAINERS",
value_delimiter = ','
)]
revoke_orderbook_maintainer: Vec<String>,
#[arg(long, env = "DEPLOY_NEW_ORDERBOOK_MAINTAINERS", value_delimiter = ',')]
new_orderbook_maintainer: Vec<String>,
}
fn parse_address(s: &str) -> anyhow::Result<fuels::types::Address> {
let trimmed = s.strip_prefix("0x").unwrap_or(s);
let bytes = hex::decode(trimmed)?;
Ok(fuels::types::Address::new(
bytes
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid address length"))?,
))
}
fn parse_identity(s: &str) -> anyhow::Result<fuels::types::Identity> {
if let Some(address) = s.strip_prefix("address:") {
Ok(fuels::types::Identity::Address(parse_address(address)?))
} else if let Some(contract_id) = s.strip_prefix("contract:") {
Ok(fuels::types::Identity::ContractId(
fuels::types::ContractId::new(*parse_address(contract_id)?),
))
} else {
anyhow::bail!(
"identity must specify its kind: `address:0x…` for a wallet that \
signs directly, or `contract:0x…` for a trade account contract \
the call is routed through"
)
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let cli = Cli::parse();
let secret_key = SecretKey::from_str(&cli.deploy_key)
.map_err(|e| anyhow::anyhow!("Failed to parse deploy key: {e}"))?;
let provider = Provider::connect(cli.fuel_rpc.as_str()).await?;
let wallet = Wallet::new(PrivateKeySigner::new(secret_key), provider);
let deploy_config: MarketsConfigPartial = load_config_from_file(&cli.deploy_config)?;
let new_proxy_owner = cli
.new_proxy_owner
.as_deref()
.map(parse_address)
.transpose()?;
let new_contract_owner = cli
.new_contract_owner
.as_deref()
.map(parse_address)
.transpose()?;
let trial_cosigner = cli
.trial_cosigner
.as_deref()
.map(parse_address)
.transpose()?;
let trial_creator = cli
.trial_creator
.as_deref()
.map(parse_identity)
.transpose()?;
let margin_cosigner = cli
.margin_cosigner
.as_deref()
.map(parse_address)
.transpose()?;
let margin_liquidator = cli
.margin_liquidator
.as_deref()
.map(parse_identity)
.transpose()?;
let new_orderbook_maintainers = cli
.new_orderbook_maintainer
.iter()
.map(|s| parse_address(s))
.collect::<anyhow::Result<Vec<_>>>()?;
let revoke_orderbook_maintainers = cli
.revoke_orderbook_maintainer
.iter()
.map(|s| parse_address(s))
.collect::<anyhow::Result<Vec<_>>>()?;
let params = DeployParams {
deploy_config,
output: cli.output,
deploy_whitelist: cli.deploy_whitelist,
deploy_blacklist: cli.deploy_blacklist,
upgrade_bytecode: cli.upgrade_bytecode,
new_proxy_owner,
new_contract_owner,
trial_cosigner,
trial_creator,
margin_tier_only: cli.margin_tier_only,
margin_cosigner,
margin_liquidator,
revoke_orderbook_maintainers,
new_orderbook_maintainers,
};
let result = o2_deploy::deploy(wallet, params).await?;
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
#[cfg(test)]
mod tests {
use super::parse_identity;
use fuels::types::Identity;
const HEX: &str = "6468f728c3c42d7805a998d7be4be93a382b3d66049e736ceb78ab4645b8a0f4";
#[test]
fn parses_address_identity() {
let identity = parse_identity(&format!("address:0x{HEX}")).unwrap();
assert!(matches!(identity, Identity::Address(_)));
}
#[test]
fn parses_contract_identity() {
let identity = parse_identity(&format!("contract:0x{HEX}")).unwrap();
assert!(matches!(identity, Identity::ContractId(_)));
}
#[test]
fn rejects_bare_value_without_kind() {
let err = parse_identity(&format!("0x{HEX}")).unwrap_err();
assert!(err.to_string().contains("must specify its kind"));
}
}