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