Skip to main content

cast/cmd/safe/
delegates.rs

1use super::{
2    service::{SafeDelegatesResponse, SafeServiceOpts},
3    signing::sign_delegate,
4};
5use alloy_network::Ethereum;
6use alloy_primitives::Address;
7use alloy_provider::Provider;
8use alloy_signer::Signer;
9use eyre::{Context, Result, ensure};
10use foundry_cli::{
11    json::{print_json_object, print_scalar},
12    opts::RpcOpts,
13    utils::LoadConfig,
14};
15use foundry_common::provider::ProviderBuilder;
16use foundry_wallets::WalletOpts;
17use reqwest::Method;
18use serde_json::json;
19use std::collections::HashSet;
20
21pub(super) async fn add(
22    safe: Address,
23    delegate: Address,
24    label: String,
25    service: SafeServiceOpts,
26    rpc: RpcOpts,
27    wallet: WalletOpts,
28) -> Result<()> {
29    ensure!(!label.trim().is_empty(), "delegate label cannot be empty");
30    let config = rpc.load_config()?;
31    let provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
32    let chain_id = provider.get_chain_id().await?;
33    let signer = wallet.signer().await?;
34    let signature = sign_delegate(&signer, delegate, chain_id).await?;
35    let url = service.endpoint(chain_id, "v2/delegates/")?;
36    let body = json!({
37        "safe": safe.to_checksum(None),
38        "delegate": delegate.to_checksum(None),
39        "delegator": signer.address().to_checksum(None),
40        "label": label,
41        "signature": signature,
42    });
43    service.empty_response(service.request(Method::POST, url).json(&body)).await?;
44    print_scalar(delegate.to_checksum(None))?;
45    Ok(())
46}
47
48pub(super) async fn list(safe: Address, service: SafeServiceOpts, rpc: RpcOpts) -> Result<()> {
49    let chain_id = if service.service_url.is_some() {
50        0
51    } else {
52        let config = rpc.load_config()?;
53        ProviderBuilder::<Ethereum>::from_config(&config)?.build()?.get_chain_id().await?
54    };
55    let mut url = service.endpoint(chain_id, "v2/delegates/")?;
56    url.query_pairs_mut().append_pair("safe", &safe.to_checksum(None));
57    let origin = url.origin();
58    let path = url.path().to_string();
59    let mut visited = HashSet::new();
60    let mut delegates = Vec::new();
61    loop {
62        ensure!(visited.insert(url.clone()), "delegate pagination contains a cycle at {url}");
63        let mut response: SafeDelegatesResponse =
64            service.response(service.request(Method::GET, url.clone())).await?;
65        delegates.append(&mut response.results);
66        let Some(next) = response.next else { break };
67        let next = url.join(&next).wrap_err("invalid delegate pagination URL")?;
68        ensure!(
69            next.origin() == origin && next.path() == path,
70            "delegate pagination URL points outside the Transaction Service endpoint: {next}"
71        );
72        url = next;
73    }
74    print_json_object(delegates)?;
75    Ok(())
76}
77
78pub(super) async fn remove(
79    safe: Address,
80    delegate: Address,
81    service: SafeServiceOpts,
82    rpc: RpcOpts,
83    wallet: WalletOpts,
84) -> Result<()> {
85    let config = rpc.load_config()?;
86    let provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
87    let chain_id = provider.get_chain_id().await?;
88    let signer = wallet.signer().await?;
89    let signature = sign_delegate(&signer, delegate, chain_id).await?;
90    let url =
91        service.endpoint(chain_id, &format!("v2/delegates/{}/", delegate.to_checksum(None)))?;
92    let body = json!({
93        "safe": safe.to_checksum(None),
94        "delegator": signer.address().to_checksum(None),
95        "signature": signature,
96    });
97    service.empty_response(service.request(Method::DELETE, url).json(&body)).await?;
98    print_scalar(delegate.to_checksum(None))?;
99    Ok(())
100}