Skip to main content

cast/cmd/tip20/
mod.rs

1use crate::{
2    cmd::send::SendTxArgs,
3    tempo,
4    tx::{SendTxOpts, TxParams},
5};
6use alloy_ens::NameOrAddress;
7use alloy_primitives::{Address, B256};
8use clap::Parser;
9use eyre::Result;
10use foundry_cli::{opts::RpcOpts, utils::get_chain};
11use foundry_wallets::{TempoAccountsWallet, WalletSigner};
12use std::str::FromStr;
13use tempo_alloy::TempoNetwork;
14
15mod create;
16pub(crate) use create::iso4217_warning_message;
17pub(crate) mod logo;
18pub(crate) mod mine;
19mod role;
20pub use role::Tip20Role;
21
22const MINE_REGISTER_MESSAGES: mine::RegisterMessages = mine::RegisterMessages {
23    no_signer: "--register requires a signer or Tempo keychain identity (for example --private-key or --from)",
24    mismatch: "registration sender mismatch: mined salt is for",
25    submitting: " on Tempo...",
26};
27
28/// TIP-20 token operations (Tempo).
29#[derive(Debug, Parser, Clone)]
30pub enum Tip20Subcommand {
31    /// Create a new TIP-20 token via the TIP20Factory.
32    #[command(visible_alias = "c")]
33    Create {
34        /// The token name (e.g. "US Dollar Coin").
35        name: String,
36
37        /// The token symbol (e.g. "USDC").
38        symbol: String,
39
40        /// The ISO 4217 currency code (e.g. "USD", "EUR", "GBP").
41        /// This field is IMMUTABLE after creation and affects fee payment
42        /// eligibility, DEX routing, and quote token pairing.
43        currency: String,
44
45        /// The TIP-20 quote token address used for exchange pricing.
46        #[arg(value_parser = NameOrAddress::from_str)]
47        quote_token: NameOrAddress,
48
49        /// The admin address to receive DEFAULT_ADMIN_ROLE on the new token.
50        #[arg(value_parser = NameOrAddress::from_str)]
51        admin: NameOrAddress,
52
53        /// A unique salt for deterministic address derivation (hex-encoded bytes32).
54        salt: B256,
55
56        /// Optional T5 logo URI for the token.
57        #[arg(long, value_name = "URI")]
58        logo_uri: Option<String>,
59
60        /// Skip the ISO 4217 currency code validation warning.
61        #[arg(long)]
62        force: bool,
63
64        #[command(flatten)]
65        send_tx: SendTxOpts,
66
67        #[command(flatten)]
68        tx: TxParams,
69    },
70
71    /// Validate a TIP-20 logo URI offline against Tempo T5 constraints.
72    LogoCheck {
73        /// The logo URI to validate. Empty string is valid.
74        #[arg(value_name = "URI")]
75        logo_uri: String,
76    },
77
78    /// Update a TIP-20 token logo URI.
79    LogoSet {
80        /// The TIP-20 token contract address.
81        #[arg(value_parser = NameOrAddress::from_str)]
82        token: NameOrAddress,
83
84        /// The new logo URI. Empty string clears the on-chain value.
85        #[arg(value_name = "URI")]
86        logo_uri: String,
87
88        #[command(flatten)]
89        send_tx: SendTxOpts,
90
91        #[command(flatten)]
92        tx: TxParams,
93    },
94
95    /// Mine a TIP-1022 salt for virtual address' master registration on Tempo.
96    #[command(visible_alias = "m")]
97    Mine {
98        /// Address that will call `registerVirtualMaster(bytes32)`.
99        #[arg(value_name = "ADDRESS")]
100        master: Address,
101
102        /// Salt to validate directly instead of mining one.
103        #[arg(long, conflicts_with_all = ["seed", "no_random"], value_name = "HEX")]
104        salt: Option<B256>,
105
106        /// Number of threads to use. Specifying 0 defaults to the number of logical cores.
107        #[arg(global = true, long, short = 'j', visible_alias = "jobs")]
108        threads: Option<usize>,
109
110        /// The random number generator's seed, used to initialize the salt search.
111        #[arg(long, value_name = "HEX")]
112        seed: Option<B256>,
113
114        /// Don't initialize the salt with a random value, and instead use the default value of 0.
115        #[arg(long, conflicts_with = "seed")]
116        no_random: bool,
117
118        /// Submit `registerVirtualMaster(bytes32)` on Tempo after finding or validating the salt.
119        #[arg(long, conflicts_with_all = ["seed", "no_random"])]
120        register: bool,
121
122        #[command(flatten)]
123        send_tx: SendTxOpts,
124
125        #[command(flatten)]
126        tx: TxParams,
127    },
128
129    /// Grant a TIP-20 role to an account.
130    ///
131    /// The sender must hold the role's admin role, which is `DEFAULT_ADMIN_ROLE` unless it was
132    /// reconfigured. A newly created token only assigns `DEFAULT_ADMIN_ROLE` to its admin, so
133    /// minting requires granting `issuer` first.
134    GrantRole {
135        /// The TIP-20 token contract address.
136        #[arg(value_parser = NameOrAddress::from_str)]
137        token: NameOrAddress,
138
139        /// The role to grant: `admin`, `issuer`, `pause`, `unpause`, `burn-blocked`, `burn-at`
140        /// (T12+), or a 32-byte role hash.
141        role: Tip20Role,
142
143        /// The account that receives the role.
144        #[arg(value_parser = NameOrAddress::from_str)]
145        account: NameOrAddress,
146
147        #[command(flatten)]
148        send_tx: SendTxOpts,
149
150        #[command(flatten)]
151        tx: TxParams,
152    },
153
154    /// Revoke a TIP-20 role from an account.
155    ///
156    /// The sender must hold the role's admin role, which is `DEFAULT_ADMIN_ROLE` unless it was
157    /// reconfigured.
158    RevokeRole {
159        /// The TIP-20 token contract address.
160        #[arg(value_parser = NameOrAddress::from_str)]
161        token: NameOrAddress,
162
163        /// The role to revoke: `admin`, `issuer`, `pause`, `unpause`, `burn-blocked`, `burn-at`
164        /// (T12+), or a 32-byte role hash.
165        role: Tip20Role,
166
167        /// The account that loses the role.
168        #[arg(value_parser = NameOrAddress::from_str)]
169        account: NameOrAddress,
170
171        #[command(flatten)]
172        send_tx: SendTxOpts,
173
174        #[command(flatten)]
175        tx: TxParams,
176    },
177
178    /// Check whether an account holds a TIP-20 role.
179    HasRole {
180        /// The TIP-20 token contract address.
181        #[arg(value_parser = NameOrAddress::from_str)]
182        token: NameOrAddress,
183
184        /// The role to check: `admin`, `issuer`, `pause`, `unpause`, `burn-blocked`, `burn-at`
185        /// (T12+), or a 32-byte role hash.
186        role: Tip20Role,
187
188        /// The account to check.
189        #[arg(value_parser = NameOrAddress::from_str)]
190        account: NameOrAddress,
191
192        #[command(flatten)]
193        rpc: RpcOpts,
194    },
195}
196
197impl Tip20Subcommand {
198    pub async fn run(self) -> Result<()> {
199        match self {
200            Self::Create {
201                name,
202                symbol,
203                currency,
204                quote_token,
205                admin,
206                salt,
207                logo_uri,
208                force,
209                send_tx,
210                tx,
211            } => {
212                create::run(
213                    name,
214                    symbol,
215                    currency,
216                    quote_token,
217                    admin,
218                    salt,
219                    logo_uri,
220                    force,
221                    send_tx,
222                    tx,
223                )
224                .await
225            }
226            Self::LogoCheck { logo_uri } => logo::check(&logo_uri),
227            Self::LogoSet { token, logo_uri, send_tx, tx } => {
228                logo::set(token, logo_uri, send_tx, tx).await
229            }
230            Self::Mine { master, salt, threads, seed, no_random, register, send_tx, tx } => {
231                let output = mine::run(master, salt, threads, seed, no_random)?;
232                if register {
233                    let msgs = &MINE_REGISTER_MESSAGES;
234                    mine::register_virtual_master(master, output.salt, send_tx, tx, false, msgs)
235                        .await?;
236                }
237                Ok(())
238            }
239            Self::GrantRole { token, role, account, send_tx, tx } => {
240                role::update(role::RoleUpdate::Grant, token, role, account, send_tx, tx).await
241            }
242            Self::RevokeRole { token, role, account, send_tx, tx } => {
243                role::update(role::RoleUpdate::Revoke, token, role, account, send_tx, tx).await
244            }
245            Self::HasRole { token, role, account, rpc } => {
246                role::has_role(token, role, account, rpc).await
247            }
248        }
249    }
250}
251
252/// Sends pre-encoded `data` to the Tempo contract at `to` through the `cast send` flow, signing
253/// with the selected Tempo session, access key, or wallet.
254pub(crate) async fn send_tip20_transaction(
255    to: Address,
256    data: Vec<u8>,
257    send_tx: SendTxOpts,
258    tx: TxParams,
259) -> Result<()> {
260    let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?;
261    SendTxArgs::contract_call(NameOrAddress::Address(to), data, send_tx, tx)
262        .run_generic::<TempoNetwork>(signer, access_key)
263        .await
264}
265
266/// Resolves the Tempo session, access key, or wallet that signs a `cast send` style transaction.
267pub(crate) async fn resolve_tip20_signer(
268    send_tx: &SendTxOpts,
269    tx: &TxParams,
270) -> Result<(Option<WalletSigner>, Option<TempoAccountsWallet>)> {
271    tempo::ensure_session_not_browser(&tx.tempo, send_tx.browser.browser)?;
272    let (config, provider) = tempo::tempo_provider(&send_tx.eth.rpc)?;
273    let chain = get_chain(config.chain, &provider).await?;
274    tempo::resolve_session_or_wallet_signer(&tx.tempo, &send_tx.eth.wallet, chain.id()).await
275}