Skip to main content

cast/cmd/safe/
delegates.rs

1use super::{
2    rpc_provider,
3    service::{SafeDelegatesResponse, SafeServiceOpts},
4    signing::sign_delegate,
5};
6use alloy_primitives::Address;
7use alloy_signer::Signer;
8use clap::Args;
9use eyre::{Context, Result, ensure};
10use foundry_cli::{
11    json::{print_json_object, print_scalar},
12    opts::RpcOpts,
13};
14use foundry_wallets::WalletOpts;
15use reqwest::Method;
16use serde_json::{Value, json};
17use std::collections::HashSet;
18
19/// CLI arguments for `cast safe add-delegate`.
20#[derive(Args, Debug)]
21pub struct AddDelegateArgs {
22    /// Safe account address.
23    safe: Address,
24
25    /// Address allowed to propose transactions.
26    delegate: Address,
27
28    /// Human-readable delegate label.
29    #[arg(long)]
30    label: String,
31
32    #[command(flatten)]
33    service: Box<SafeServiceOpts>,
34
35    #[command(flatten)]
36    rpc: Box<RpcOpts>,
37
38    #[command(flatten)]
39    wallet: Box<WalletOpts>,
40}
41
42impl AddDelegateArgs {
43    pub(super) async fn run(self) -> Result<()> {
44        let Self { safe, delegate, label, service, rpc, wallet } = self;
45        ensure!(!label.trim().is_empty(), "delegate label cannot be empty");
46        let body = json!({ "delegate": delegate.to_checksum(None), "label": label });
47        submit_delegate(
48            &service,
49            &rpc,
50            &wallet,
51            safe,
52            delegate,
53            Method::POST,
54            "v2/delegates/",
55            body,
56        )
57        .await
58    }
59}
60
61/// CLI arguments for `cast safe list-delegates`.
62#[derive(Args, Debug)]
63pub struct ListDelegatesArgs {
64    /// Safe account address.
65    safe: Address,
66
67    #[command(flatten)]
68    service: Box<SafeServiceOpts>,
69
70    #[command(flatten)]
71    rpc: Box<RpcOpts>,
72}
73
74impl ListDelegatesArgs {
75    pub(super) async fn run(self) -> Result<()> {
76        let Self { safe, service, rpc } = self;
77        let chain_id = if service.service_url.is_some() { 0 } else { rpc_provider(&rpc).await?.1 };
78        let mut url = service.endpoint(chain_id, "v2/delegates/")?;
79        url.query_pairs_mut().append_pair("safe", &safe.to_checksum(None));
80        let origin = url.origin();
81        let path = url.path().to_string();
82        let mut visited = HashSet::new();
83        let mut delegates = Vec::new();
84        loop {
85            ensure!(visited.insert(url.clone()), "delegate pagination contains a cycle at {url}");
86            let mut response: SafeDelegatesResponse =
87                service.response(service.request(Method::GET, url.clone())).await?;
88            delegates.append(&mut response.results);
89            let Some(next) = response.next else { break };
90            let next = url.join(&next).wrap_err("invalid delegate pagination URL")?;
91            ensure!(
92                next.origin() == origin && next.path() == path,
93                "delegate pagination URL points outside the Transaction Service endpoint: {next}"
94            );
95            url = next;
96        }
97        print_json_object(delegates)
98    }
99}
100
101/// CLI arguments for `cast safe remove-delegate`.
102#[derive(Args, Debug)]
103pub struct RemoveDelegateArgs {
104    /// Safe account address.
105    safe: Address,
106
107    /// Delegate address to remove.
108    delegate: Address,
109
110    #[command(flatten)]
111    service: Box<SafeServiceOpts>,
112
113    #[command(flatten)]
114    rpc: Box<RpcOpts>,
115
116    #[command(flatten)]
117    wallet: Box<WalletOpts>,
118}
119
120impl RemoveDelegateArgs {
121    pub(super) async fn run(self) -> Result<()> {
122        let Self { safe, delegate, service, rpc, wallet } = self;
123        let path = format!("v2/delegates/{}/", delegate.to_checksum(None));
124        submit_delegate(&service, &rpc, &wallet, safe, delegate, Method::DELETE, &path, json!({}))
125            .await
126    }
127}
128
129/// Signs the delegate TOTP message with the wallet and submits `body` (extended with the Safe,
130/// delegator and signature) to the Transaction Service, printing the delegate on success.
131#[allow(clippy::too_many_arguments)]
132async fn submit_delegate(
133    service: &SafeServiceOpts,
134    rpc: &RpcOpts,
135    wallet: &WalletOpts,
136    safe: Address,
137    delegate: Address,
138    method: Method,
139    path: &str,
140    mut body: Value,
141) -> Result<()> {
142    let (_, chain_id) = rpc_provider(rpc).await?;
143    let signer = wallet.signer().await?;
144    let signature = sign_delegate(&signer, delegate, chain_id).await?;
145    body["safe"] = safe.to_checksum(None).into();
146    body["delegator"] = signer.address().to_checksum(None).into();
147    body["signature"] = signature.into();
148    let url = service.endpoint(chain_id, path)?;
149    service.empty_response(service.request(method, url).json(&body)).await?;
150    print_scalar(delegate.to_checksum(None))
151}