cast/cmd/safe/
delegates.rs1use 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}