cast/cmd/safe/
delegates.rs1use 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#[derive(Args, Debug)]
21pub struct AddDelegateArgs {
22 safe: Address,
24
25 delegate: Address,
27
28 #[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#[derive(Args, Debug)]
63pub struct ListDelegatesArgs {
64 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#[derive(Args, Debug)]
103pub struct RemoveDelegateArgs {
104 safe: Address,
106
107 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#[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}