use crate::config::TrezuConfig;
use crate::types::*;
use color_eyre::eyre::{Result, WrapErr, eyre};
pub struct ApiClient {
client: reqwest::blocking::Client,
base_url: String,
auth_token: Option<String>,
}
impl ApiClient {
pub fn new(config: &TrezuConfig) -> Self {
Self {
client: reqwest::blocking::Client::builder()
.cookie_store(true)
.build()
.expect("Failed to create HTTP client"),
base_url: config.api_base.clone(),
auth_token: config.auth_token.clone(),
}
}
fn url(&self, path: &str) -> String {
let base_url = self.base_url.trim_end_matches('/');
let path = path.trim_start_matches('/');
format!("{}/api/{}", base_url, path)
}
fn get(&self, path: &str) -> reqwest::blocking::RequestBuilder {
let mut req = self.client.get(self.url(path));
if let Some(token) = &self.auth_token {
req = req.header("Cookie", format!("auth_token={}", token));
}
req
}
fn post(&self, path: &str) -> reqwest::blocking::RequestBuilder {
let mut req = self.client.post(self.url(path));
if let Some(token) = &self.auth_token {
req = req.header("Cookie", format!("auth_token={}", token));
}
req
}
fn delete(&self, path: &str) -> reqwest::blocking::RequestBuilder {
let mut req = self.client.delete(self.url(path));
if let Some(token) = &self.auth_token {
req = req.header("Cookie", format!("auth_token={}", token));
}
req
}
#[tracing::instrument(name = "Getting auth challenge ...", skip_all)]
pub fn get_challenge(&self) -> Result<ChallengeResponse> {
let resp = self
.post("/auth/challenge")
.send()
.wrap_err("Failed to get auth challenge")?;
if !resp.status().is_success() {
return Err(eyre!("Challenge request failed: {}", resp.status()));
}
resp.json().wrap_err("Failed to parse challenge response")
}
#[tracing::instrument(name = "Logging in ...", skip_all)]
pub fn login(&self, request: &LoginRequest) -> Result<(MeResponse, String)> {
let resp = self
.client
.post(self.url("/auth/login"))
.json(request)
.send()
.wrap_err("Failed to send login request")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("Login failed ({}): {}", status, body));
}
let token = resp
.headers()
.get_all("set-cookie")
.iter()
.find_map(|v| {
let s = v.to_str().ok()?;
if s.starts_with("auth_token=") {
Some(
s.split(';')
.next()?
.trim_start_matches("auth_token=")
.to_string(),
)
} else {
None
}
})
.ok_or_else(|| eyre!("No auth token in login response"))?;
let me: MeResponse = resp.json().wrap_err("Failed to parse login response")?;
Ok((me, token))
}
#[tracing::instrument(name = "Getting user info ...", skip_all)]
pub fn get_me(&self) -> Result<MeResponse> {
let resp = self
.get("/auth/me")
.send()
.wrap_err("Failed to check auth status")?;
if !resp.status().is_success() {
return Err(eyre!("Not authenticated ({})", resp.status()));
}
resp.json().wrap_err("Failed to parse /me response")
}
#[tracing::instrument(name = "Logging out ...", skip_all)]
pub fn logout(&self) -> Result<()> {
let resp = self
.post("/auth/logout")
.send()
.wrap_err("Failed to logout")?;
if !resp.status().is_success() {
return Err(eyre!("Logout failed: {}", resp.status()));
}
Ok(())
}
#[tracing::instrument(name = "Accepting terms of service ...", skip_all)]
pub fn accept_terms(&self) -> Result<()> {
let resp = self
.post("/auth/accept-terms")
.send()
.wrap_err("Failed to accept terms")?;
if !resp.status().is_success() {
return Err(eyre!("Accept terms failed: {}", resp.status()));
}
Ok(())
}
#[tracing::instrument(name = "Fetching treasuries ...", skip_all)]
pub fn list_treasuries(&self, account_id: &str) -> Result<Vec<Treasury>> {
let resp = self
.get("/user/treasuries")
.query(&[("accountId", account_id)])
.send()
.wrap_err("Failed to list treasuries")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("List treasuries failed ({}): {}", status, body));
}
resp.json().wrap_err("Failed to parse treasuries response")
}
#[tracing::instrument(name = "Fetching treasury config ...", skip_all)]
pub fn get_treasury_config(&self, treasury_id: &str) -> Result<TreasuryConfig> {
let resp = self
.get("/treasury/config")
.query(&[("treasuryId", treasury_id)])
.send()
.wrap_err("Failed to get treasury config")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("Get treasury config failed ({}): {}", status, body));
}
resp.json()
.wrap_err("Failed to parse treasury config response")
}
#[tracing::instrument(name = "Fetching treasury policy ...", skip_all)]
pub fn get_treasury_policy(&self, treasury_id: &str) -> Result<Policy> {
let resp = self
.get("/treasury/policy")
.query(&[("treasuryId", treasury_id)])
.send()
.wrap_err("Failed to get treasury policy")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("Get treasury policy failed ({}): {}", status, body));
}
resp.json()
.wrap_err("Failed to parse treasury policy response")
}
#[tracing::instrument(name = "Fetching assets ...", skip_all)]
pub fn get_assets(&self, account_id: &str) -> Result<Vec<SimplifiedToken>> {
let resp = self
.get("/user/assets")
.query(&[("accountId", account_id)])
.send()
.wrap_err("Failed to get assets")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("Get assets failed ({}): {}", status, body));
}
resp.json().wrap_err("Failed to parse assets response")
}
#[tracing::instrument(name = "Listing proposals ...", skip_all)]
pub fn list_proposals(
&self,
dao_id: &str,
status: Option<&str>,
page: Option<usize>,
page_size: Option<usize>,
) -> Result<PaginatedProposals> {
let mut req = self.get(&format!("/proposals/{}", dao_id));
if let Some(s) = status {
req = req.query(&[("statuses", s)]);
}
if let Some(p) = page {
req = req.query(&[("page", &p.to_string())]);
}
if let Some(ps) = page_size {
req = req.query(&[("pageSize", &ps.to_string())]);
}
let resp = req.send().wrap_err("Failed to list proposals")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("List proposals failed ({}): {}", status, body));
}
resp.json().wrap_err("Failed to parse proposals response")
}
#[tracing::instrument(name = "Fetching proposal ...", skip_all)]
pub fn get_proposal(&self, dao_id: &str, proposal_id: u64) -> Result<Proposal> {
let resp = self
.get(&format!("/proposal/{}/{}", dao_id, proposal_id))
.send()
.wrap_err("Failed to get proposal")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("Get proposal failed ({}): {}", status, body));
}
resp.json().wrap_err("Failed to parse proposal response")
}
#[tracing::instrument(name = "Fetching address book ...", skip_all)]
pub fn list_address_book(&self, dao_id: &str) -> Result<Vec<AddressBookEntry>> {
let resp = self
.get("/address-book")
.query(&[("daoId", dao_id)])
.send()
.wrap_err("Failed to list address book")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("List address book failed ({}): {}", status, body));
}
resp.json()
.wrap_err("Failed to parse address book response")
}
#[tracing::instrument(name = "Adding address book entry ...", skip_all)]
pub fn create_address_book_entries(
&self,
dao_id: &str,
entries: Vec<CreateAddressBookEntryRequest>,
) -> Result<()> {
let body = serde_json::json!({
"daoId": dao_id,
"entries": entries,
});
let resp = self
.post("/address-book")
.json(&body)
.send()
.wrap_err("Failed to create address book entries")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!(
"Create address book entries failed ({}): {}",
status,
body
));
}
Ok(())
}
#[tracing::instrument(name = "Removing address book entry ...", skip_all)]
pub fn delete_address_book_entries(&self, ids: Vec<uuid::Uuid>) -> Result<()> {
let body = serde_json::json!({ "ids": ids });
let resp = self
.delete("/address-book")
.json(&body)
.send()
.wrap_err("Failed to delete address book entries")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!(
"Delete address book entries failed ({}): {}",
status,
body
));
}
Ok(())
}
#[tracing::instrument(name = "Fetching recent activity ...", skip_all)]
pub fn get_recent_activity(
&self,
account_id: &str,
limit: Option<usize>,
) -> Result<Vec<BalanceChange>> {
use crate::types::RecentActivityResponse;
let mut req = self
.get("/recent-activity")
.query(&[("accountId", account_id)]);
if let Some(l) = limit {
req = req.query(&[("limit", &l.to_string())]);
}
let resp = req.send().wrap_err("Failed to get recent activity")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("Get recent activity failed ({}): {}", status, body));
}
let wrapper: RecentActivityResponse = resp
.json()
.wrap_err("Failed to parse recent activity response")?;
Ok(wrapper.data)
}
#[tracing::instrument(name = "Getting intents quote ...", skip_all)]
pub fn get_intents_quote(&self, request: &serde_json::Value) -> Result<serde_json::Value> {
let resp = self
.post("/intents/quote")
.json(request)
.send()
.wrap_err("Failed to get intents quote")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("Get intents quote failed ({}): {}", status, body));
}
resp.json()
.wrap_err("Failed to parse intents quote response")
}
#[tracing::instrument(name = "Generating intent ...", skip_all)]
pub fn generate_intent(&self, request: &serde_json::Value) -> Result<serde_json::Value> {
let resp = self
.post("/confidential-intents/generate-intent")
.json(request)
.send()
.wrap_err("Failed to generate intent")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().unwrap_or_default();
return Err(eyre!("Generate intent failed ({}): {}", status, body));
}
resp.json()
.wrap_err("Failed to parse generate intent response")
}
#[tracing::instrument(name = "Relaying delegate action ...", skip_all)]
pub fn relay_delegate_action(&self, body: &serde_json::Value) -> Result<serde_json::Value> {
let resp = self
.post("/relay/delegate-action")
.json(body)
.send()
.wrap_err("Failed to relay delegate action")?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().unwrap_or_default();
return Err(eyre!("Relay failed ({}): {}", status, text));
}
resp.json().wrap_err("Failed to parse relay response")
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CreateAddressBookEntryRequest {
pub name: String,
pub networks: Vec<String>,
pub address: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}