1use crate::{
2 cmd::tip20::{resolve_tip20_signer, send_tip20_transaction},
3 tempo::print_payload,
4 tx::{SendTxOpts, TxParams},
5};
6use alloy_ens::NameOrAddress;
7use alloy_primitives::Address;
8use clap::{Parser, ValueEnum};
9use eyre::Result;
10use foundry_cli::{
11 opts::RpcOpts,
12 utils::{LoadConfig, get_provider},
13};
14use serde_json::json;
15use std::str::FromStr;
16use tempo_contracts::precompiles::{ITIP403Registry, TIP403_REGISTRY_ADDRESS};
17use tempo_primitives::TempoAddressExt;
18
19#[derive(Debug, Parser, Clone)]
24pub enum Tip403Subcommand {
25 Create {
27 #[arg(value_enum)]
29 policy_type: PolicyKind,
30
31 #[arg(long, value_parser = NameOrAddress::from_str)]
33 admin: NameOrAddress,
34
35 #[arg(long = "member", value_name = "ADDRESS", value_parser = NameOrAddress::from_str)]
37 accounts: Vec<NameOrAddress>,
38
39 #[command(flatten)]
40 send_tx: SendTxOpts,
41
42 #[command(flatten)]
43 tx: TxParams,
44 },
45
46 Whitelist {
48 #[command(flatten)]
49 args: MembershipArgs,
50 },
51
52 Blacklist {
54 #[command(flatten)]
55 args: MembershipArgs,
56 },
57
58 Info {
60 policy_id: u64,
62
63 #[command(flatten)]
64 rpc: RpcOpts,
65 },
66
67 Check {
69 policy_id: u64,
71
72 #[arg(value_parser = NameOrAddress::from_str)]
74 address: NameOrAddress,
75
76 #[arg(long, value_enum)]
78 role: Option<PolicyRole>,
79
80 #[command(flatten)]
81 rpc: RpcOpts,
82 },
83}
84
85#[derive(Debug, Clone, clap::Args)]
86pub struct MembershipArgs {
87 #[arg(value_enum)]
89 pub action: MembershipAction,
90
91 pub policy_id: u64,
93
94 #[arg(value_parser = NameOrAddress::from_str)]
96 pub account: NameOrAddress,
97
98 #[command(flatten)]
99 pub send_tx: SendTxOpts,
100
101 #[command(flatten)]
102 pub tx: TxParams,
103}
104
105#[derive(Debug, Clone, Copy, ValueEnum)]
106pub enum PolicyKind {
107 Whitelist,
108 Blacklist,
109}
110
111#[derive(Debug, Clone, Copy, ValueEnum)]
112pub enum MembershipAction {
113 Add,
114 Remove,
115}
116
117#[derive(Debug, Clone, Copy, ValueEnum)]
118pub enum PolicyRole {
119 Sender,
120 Recipient,
121 MintRecipient,
122}
123
124impl Tip403Subcommand {
125 pub async fn run(self) -> Result<()> {
126 match self {
127 Self::Create { policy_type, admin, accounts, send_tx, tx } => {
128 create(policy_type, admin, accounts, send_tx, tx).await
129 }
130 Self::Whitelist { args } => modify(PolicyKind::Whitelist, args).await,
131 Self::Blacklist { args } => modify(PolicyKind::Blacklist, args).await,
132 Self::Info { policy_id, rpc } => info(policy_id, rpc).await,
133 Self::Check { policy_id, address, role, rpc } => {
134 check(policy_id, address, role, rpc).await
135 }
136 }
137 }
138}
139
140async fn create(
141 policy_type: PolicyKind,
142 admin: NameOrAddress,
143 accounts: Vec<NameOrAddress>,
144 send_tx: SendTxOpts,
145 tx: TxParams,
146) -> Result<()> {
147 let config = send_tx.eth.rpc.load_config()?;
148 let provider = get_provider(&config)?;
149 let admin = admin.resolve(&provider).await?;
150
151 let mut members = Vec::with_capacity(accounts.len());
152 for account in accounts {
153 let account = account.resolve(&provider).await?;
154 warn_if_virtual(account)?;
155 members.push(account);
156 }
157
158 let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, &provider);
161 let policy_type_sol = policy_type.to_sol();
162 let expected_id = if members.is_empty() {
163 registry.createPolicy(admin, policy_type_sol).call().await?
164 } else {
165 registry.createPolicyWithAccounts(admin, policy_type_sol, members.clone()).call().await?
166 };
167 sh_status!(
169 "Expected policy ID: {expected_id} (only if this tx is mined before any other policy \
170 creation; read the PolicyCreated event for the authoritative ID)"
171 )?;
172
173 let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?;
174 let type_arg = (policy_type_sol as u8).to_string();
175 let (sig, args) = if members.is_empty() {
176 ("createPolicy(address,uint8)", vec![admin.to_string(), type_arg])
177 } else {
178 let members =
179 format!("[{}]", members.iter().map(Address::to_string).collect::<Vec<_>>().join(","));
180 (
181 "createPolicyWithAccounts(address,uint8,address[])",
182 vec![admin.to_string(), type_arg, members],
183 )
184 };
185 send_tip20_transaction(
186 NameOrAddress::Address(TIP403_REGISTRY_ADDRESS),
187 sig,
188 args,
189 send_tx,
190 tx,
191 signer,
192 access_key,
193 )
194 .await
195}
196
197async fn modify(kind: PolicyKind, args: MembershipArgs) -> Result<()> {
198 let MembershipArgs { action, policy_id, account, send_tx, tx } = args;
199 let config = send_tx.eth.rpc.load_config()?;
200 let provider = get_provider(&config)?;
201 let account = account.resolve(&provider).await?;
202 warn_if_virtual(account)?;
203
204 let flag = matches!(action, MembershipAction::Add);
205 let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?;
206 let sig = match kind {
207 PolicyKind::Whitelist => "modifyPolicyWhitelist(uint64,address,bool)",
208 PolicyKind::Blacklist => "modifyPolicyBlacklist(uint64,address,bool)",
209 };
210 send_tip20_transaction(
211 NameOrAddress::Address(TIP403_REGISTRY_ADDRESS),
212 sig,
213 vec![policy_id.to_string(), account.to_string(), flag.to_string()],
214 send_tx,
215 tx,
216 signer,
217 access_key,
218 )
219 .await
220}
221
222async fn info(policy_id: u64, rpc: RpcOpts) -> Result<()> {
223 let config = rpc.load_config()?;
224 let provider = get_provider(&config)?;
225 let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider);
226 let builtin = builtin_label(policy_id);
227
228 if !registry.policyExists(policy_id).call().await? {
229 let payload = json!({ "policy_id": policy_id, "exists": false, "builtin": builtin });
230 return print_payload(payload, |_| sh_println!("Policy {policy_id} does not exist"));
231 }
232
233 let data = registry.policyData(policy_id).call().await?;
234 let payload = json!({
235 "policy_id": policy_id,
236 "exists": true,
237 "builtin": builtin,
238 "policy_type": policy_type_label(data.policyType),
239 "admin": format!("{}", data.admin),
240 });
241 print_payload(payload, |payload| {
242 sh_println!(
243 "Policy ID: {}\n\
244 Built-in: {}\n\
245 Type: {}\n\
246 Admin: {}",
247 payload["policy_id"],
248 payload["builtin"].as_str().unwrap_or("no"),
249 payload["policy_type"].as_str().unwrap_or_default(),
250 payload["admin"].as_str().unwrap_or_default(),
251 )
252 })
253}
254
255async fn check(
256 policy_id: u64,
257 address: NameOrAddress,
258 role: Option<PolicyRole>,
259 rpc: RpcOpts,
260) -> Result<()> {
261 let config = rpc.load_config()?;
262 let provider = get_provider(&config)?;
263 let address = address.resolve(&provider).await?;
264 let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider);
265 let authorized = match role {
266 None => registry.isAuthorized(policy_id, address).call().await?,
267 Some(PolicyRole::Sender) => registry.isAuthorizedSender(policy_id, address).call().await?,
268 Some(PolicyRole::Recipient) => {
269 registry.isAuthorizedRecipient(policy_id, address).call().await?
270 }
271 Some(PolicyRole::MintRecipient) => {
272 registry.isAuthorizedMintRecipient(policy_id, address).call().await?
273 }
274 };
275
276 let payload = json!({
277 "policy_id": policy_id,
278 "address": format!("{address}"),
279 "role": role_label(role),
280 "authorized": authorized,
281 });
282 print_payload(payload, |payload| {
283 sh_println!(
284 "Policy ID: {}\n\
285 Address: {}\n\
286 Role: {}\n\
287 Authorized: {}",
288 payload["policy_id"],
289 payload["address"].as_str().unwrap_or_default(),
290 payload["role"].as_str().unwrap_or_default(),
291 payload["authorized"].as_bool().unwrap_or_default(),
292 )
293 })
294}
295
296fn warn_if_virtual(account: Address) -> Result<()> {
298 if account.is_virtual() {
299 sh_warn!(
300 "{account} looks like a TIP-1022 virtual address; on T3+ chains it is rejected as a \
301 literal policy member. Resolve it to its master with `cast vaddr resolve {account}`."
302 )?;
303 }
304 Ok(())
305}
306
307impl PolicyKind {
308 const fn to_sol(self) -> ITIP403Registry::PolicyType {
309 match self {
310 Self::Whitelist => ITIP403Registry::PolicyType::WHITELIST,
311 Self::Blacklist => ITIP403Registry::PolicyType::BLACKLIST,
312 }
313 }
314}
315
316const fn policy_type_label(policy_type: ITIP403Registry::PolicyType) -> &'static str {
317 match policy_type {
318 ITIP403Registry::PolicyType::WHITELIST => "whitelist",
319 ITIP403Registry::PolicyType::BLACKLIST => "blacklist",
320 ITIP403Registry::PolicyType::COMPOUND => "compound",
321 _ => "unknown",
322 }
323}
324
325const fn builtin_label(policy_id: u64) -> Option<&'static str> {
326 match policy_id {
327 0 => Some("reject-all"),
328 1 => Some("allow-all"),
329 _ => None,
330 }
331}
332
333const fn role_label(role: Option<PolicyRole>) -> &'static str {
334 match role {
335 None => "transfer",
336 Some(PolicyRole::Sender) => "sender",
337 Some(PolicyRole::Recipient) => "recipient",
338 Some(PolicyRole::MintRecipient) => "mint-recipient",
339 }
340}