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 (
179 "createPolicyWithAccounts(address,uint8,address[])",
180 vec![admin.to_string(), type_arg, address_array(&members)],
181 )
182 };
183 send_tip20_transaction(
184 NameOrAddress::Address(TIP403_REGISTRY_ADDRESS),
185 sig,
186 args,
187 send_tx,
188 tx,
189 signer,
190 access_key,
191 )
192 .await
193}
194
195async fn modify(kind: PolicyKind, args: MembershipArgs) -> Result<()> {
196 let MembershipArgs { action, policy_id, account, send_tx, tx } = args;
197 let config = send_tx.eth.rpc.load_config()?;
198 let provider = get_provider(&config)?;
199 let account = account.resolve(&provider).await?;
200 warn_if_virtual(account)?;
201
202 let flag = matches!(action, MembershipAction::Add);
203 let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?;
204 let sig = match kind {
205 PolicyKind::Whitelist => "modifyPolicyWhitelist(uint64,address,bool)",
206 PolicyKind::Blacklist => "modifyPolicyBlacklist(uint64,address,bool)",
207 };
208 send_tip20_transaction(
209 NameOrAddress::Address(TIP403_REGISTRY_ADDRESS),
210 sig,
211 vec![policy_id.to_string(), account.to_string(), flag.to_string()],
212 send_tx,
213 tx,
214 signer,
215 access_key,
216 )
217 .await
218}
219
220async fn info(policy_id: u64, rpc: RpcOpts) -> Result<()> {
221 let config = rpc.load_config()?;
222 let provider = get_provider(&config)?;
223 let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider);
224 let builtin = builtin_label(policy_id);
225
226 if !registry.policyExists(policy_id).call().await? {
227 let payload = json!({ "policy_id": policy_id, "exists": false, "builtin": builtin });
228 return print_payload(payload, |_| sh_println!("Policy {policy_id} does not exist"));
229 }
230
231 let data = registry.policyData(policy_id).call().await?;
232 let payload = json!({
233 "policy_id": policy_id,
234 "exists": true,
235 "builtin": builtin,
236 "policy_type": policy_type_label(data.policyType),
237 "admin": format!("{}", data.admin),
238 });
239 print_payload(payload, |payload| {
240 sh_println!(
241 "Policy ID: {}\n\
242 Built-in: {}\n\
243 Type: {}\n\
244 Admin: {}",
245 payload["policy_id"],
246 payload["builtin"].as_str().unwrap_or("no"),
247 payload["policy_type"].as_str().unwrap_or_default(),
248 payload["admin"].as_str().unwrap_or_default(),
249 )
250 })
251}
252
253async fn check(
254 policy_id: u64,
255 address: NameOrAddress,
256 role: Option<PolicyRole>,
257 rpc: RpcOpts,
258) -> Result<()> {
259 let config = rpc.load_config()?;
260 let provider = get_provider(&config)?;
261 let address = address.resolve(&provider).await?;
262 let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider);
263 let authorized = match role {
264 None => registry.isAuthorized(policy_id, address).call().await?,
265 Some(PolicyRole::Sender) => registry.isAuthorizedSender(policy_id, address).call().await?,
266 Some(PolicyRole::Recipient) => {
267 registry.isAuthorizedRecipient(policy_id, address).call().await?
268 }
269 Some(PolicyRole::MintRecipient) => {
270 registry.isAuthorizedMintRecipient(policy_id, address).call().await?
271 }
272 };
273
274 let payload = json!({
275 "policy_id": policy_id,
276 "address": format!("{address}"),
277 "role": role_label(role),
278 "authorized": authorized,
279 });
280 print_payload(payload, |payload| {
281 sh_println!(
282 "Policy ID: {}\n\
283 Address: {}\n\
284 Role: {}\n\
285 Authorized: {}",
286 payload["policy_id"],
287 payload["address"].as_str().unwrap_or_default(),
288 payload["role"].as_str().unwrap_or_default(),
289 payload["authorized"].as_bool().unwrap_or_default(),
290 )
291 })
292}
293
294fn warn_if_virtual(account: Address) -> Result<()> {
296 if account.is_virtual() {
297 sh_warn!(
298 "{account} looks like a TIP-1022 virtual address; on T3+ chains it is rejected as a \
299 literal policy member. Resolve it to its master with `cast vaddr resolve {account}`."
300 )?;
301 }
302 Ok(())
303}
304
305fn address_array(accounts: &[Address]) -> String {
306 format!("[{}]", accounts.iter().map(Address::to_string).collect::<Vec<_>>().join(","))
307}
308
309impl PolicyKind {
310 const fn to_sol(self) -> ITIP403Registry::PolicyType {
311 match self {
312 Self::Whitelist => ITIP403Registry::PolicyType::WHITELIST,
313 Self::Blacklist => ITIP403Registry::PolicyType::BLACKLIST,
314 }
315 }
316}
317
318const fn policy_type_label(policy_type: ITIP403Registry::PolicyType) -> &'static str {
319 match policy_type {
320 ITIP403Registry::PolicyType::WHITELIST => "whitelist",
321 ITIP403Registry::PolicyType::BLACKLIST => "blacklist",
322 ITIP403Registry::PolicyType::COMPOUND => "compound",
323 _ => "unknown",
324 }
325}
326
327const fn builtin_label(policy_id: u64) -> Option<&'static str> {
328 match policy_id {
329 0 => Some("reject-all"),
330 1 => Some("allow-all"),
331 _ => None,
332 }
333}
334
335const fn role_label(role: Option<PolicyRole>) -> &'static str {
336 match role {
337 None => "transfer",
338 Some(PolicyRole::Sender) => "sender",
339 Some(PolicyRole::Recipient) => "recipient",
340 Some(PolicyRole::MintRecipient) => "mint-recipient",
341 }
342}