Skip to main content

cast/cmd/
tip20.rs

1use crate::{
2    cmd::{erc20::build_provider_with_signer, send::cast_send},
3    tx::{CastTxSender, SendTxOpts},
4};
5use alloy_ens::NameOrAddress;
6use alloy_network::{Network, TransactionBuilder};
7use alloy_primitives::{B256, U256};
8use alloy_provider::Provider;
9use alloy_sol_types::sol;
10use clap::{Args, Parser};
11use foundry_cli::{
12    opts::{RpcOpts, TempoOpts},
13    utils::{LoadConfig, get_chain},
14};
15use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder};
16use std::str::FromStr;
17use tempo_alloy::TempoNetwork;
18use tempo_contracts::precompiles::{TIP20_FACTORY_ADDRESS, is_iso4217_currency};
19
20sol! {
21    #[sol(rpc)]
22    interface ITIP20Factory {
23        function createToken(
24            string memory name,
25            string memory symbol,
26            string memory currency,
27            address quoteToken,
28            address admin,
29            bytes32 salt
30        ) external returns (address token);
31    }
32}
33
34/// Returns a warning message for non-ISO 4217 currency codes used in TIP-20 token creation.
35pub(crate) fn iso4217_warning_message(currency: &str) -> String {
36    let hyperlink = |url: &str| format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\");
37    let tip20_docs = hyperlink("https://docs.tempo.xyz/protocol/tip20/overview");
38    let iso_docs = hyperlink("https://www.iso.org/iso-4217-currency-codes.html");
39
40    format!(
41        "\"{currency}\" is not a recognized ISO 4217 currency code.\n\
42         \n\
43         If the token you are trying to deploy is a fiat-backed stablecoin, Tempo strongly\n\
44         recommends that the currency code field be the ISO-4217 currency code of the fiat\n\
45         currency your token tracks (e.g. \"USD\", \"EUR\", \"GBP\").\n\
46         \n\
47         The currency field is IMMUTABLE after token creation and affects fee payment\n\
48         eligibility, DEX routing, and quote token pairing. Only \"USD\"-denominated tokens\n\
49         can be used to pay transaction fees on Tempo.\n\
50         \n\
51         Learn more:\n  \
52         - Tempo TIP-20 docs: {tip20_docs}\n  \
53         - ISO 4217 standard: {iso_docs}"
54    )
55}
56
57/// TIP-20 token operations (Tempo).
58#[derive(Debug, Parser, Clone)]
59pub enum Tip20Subcommand {
60    /// Create a new TIP-20 token via the TIP20Factory.
61    #[command(visible_alias = "c")]
62    Create {
63        /// The token name (e.g. "US Dollar Coin").
64        name: String,
65
66        /// The token symbol (e.g. "USDC").
67        symbol: String,
68
69        /// The ISO 4217 currency code (e.g. "USD", "EUR", "GBP").
70        /// This field is IMMUTABLE after creation and affects fee payment
71        /// eligibility, DEX routing, and quote token pairing.
72        currency: String,
73
74        /// The TIP-20 quote token address used for exchange pricing.
75        #[arg(value_parser = NameOrAddress::from_str)]
76        quote_token: NameOrAddress,
77
78        /// The admin address to receive DEFAULT_ADMIN_ROLE on the new token.
79        #[arg(value_parser = NameOrAddress::from_str)]
80        admin: NameOrAddress,
81
82        /// A unique salt for deterministic address derivation (hex-encoded bytes32).
83        salt: B256,
84
85        /// Skip the ISO 4217 currency code validation warning.
86        #[arg(long)]
87        force: bool,
88
89        #[command(flatten)]
90        send_tx: SendTxOpts,
91
92        #[command(flatten)]
93        tx: Tip20TxOpts,
94    },
95}
96
97/// Transaction options for TIP-20 operations.
98#[derive(Debug, Clone, Args)]
99#[command(next_help_heading = "Transaction options")]
100pub struct Tip20TxOpts {
101    /// Gas limit for the transaction.
102    #[arg(long)]
103    pub gas_limit: Option<U256>,
104
105    /// Gas price or max fee per gas for the transaction.
106    #[arg(long)]
107    pub gas_price: Option<U256>,
108
109    /// Max priority fee per gas (EIP-1559).
110    #[arg(long)]
111    pub priority_gas_price: Option<U256>,
112
113    /// Nonce for the transaction.
114    #[arg(long)]
115    pub nonce: Option<U256>,
116
117    #[command(flatten)]
118    pub tempo: TempoOpts,
119}
120
121impl Tip20TxOpts {
122    /// Applies gas, fee, nonce, and Tempo options to a transaction request.
123    fn apply<N: Network>(&self, tx: &mut N::TransactionRequest, legacy: bool)
124    where
125        N::TransactionRequest: FoundryTransactionBuilder<N>,
126    {
127        if let Some(gas_limit) = self.gas_limit {
128            tx.set_gas_limit(gas_limit.to());
129        }
130
131        if let Some(gas_price) = self.gas_price {
132            if legacy {
133                tx.set_gas_price(gas_price.to());
134            } else {
135                tx.set_max_fee_per_gas(gas_price.to());
136            }
137        }
138
139        if !legacy && let Some(priority_fee) = self.priority_gas_price {
140            tx.set_max_priority_fee_per_gas(priority_fee.to());
141        }
142
143        self.tempo.apply::<N>(tx, self.nonce.map(|n| n.to()));
144    }
145}
146
147impl Tip20Subcommand {
148    fn rpc_opts(&self) -> &RpcOpts {
149        match self {
150            Self::Create { send_tx, .. } => &send_tx.eth.rpc,
151        }
152    }
153
154    pub async fn run(self) -> eyre::Result<()> {
155        let (signer, tempo_access_key) = match &self {
156            Self::Create { send_tx, .. } => {
157                if send_tx.eth.wallet.from.is_some() {
158                    send_tx.eth.wallet.maybe_signer().await?
159                } else {
160                    (None, None)
161                }
162            }
163        };
164
165        let config = self.rpc_opts().load_config()?;
166
167        match self {
168            Self::Create {
169                name,
170                symbol,
171                currency,
172                quote_token,
173                admin,
174                salt,
175                force,
176                send_tx,
177                tx: tx_opts,
178            } => {
179                if !is_iso4217_currency(&currency) && !force {
180                    sh_warn!("{}", iso4217_warning_message(&currency))?;
181                    let response: String = foundry_common::prompt!("\nContinue anyway? [y/N] ")?;
182                    if !matches!(response.trim(), "y" | "Y") {
183                        sh_println!("Aborted.")?;
184                        return Ok(());
185                    }
186                }
187
188                let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
189                let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
190                let quote_token_addr = quote_token.resolve(&provider).await?;
191                let admin_addr = admin.resolve(&provider).await?;
192
193                let mut tx = ITIP20Factory::new(TIP20_FACTORY_ADDRESS, &provider)
194                    .createToken(name, symbol, currency, quote_token_addr, admin_addr, salt)
195                    .into_transaction_request();
196
197                tx_opts.apply::<TempoNetwork>(
198                    &mut tx,
199                    get_chain(config.chain, &provider).await?.is_legacy(),
200                );
201
202                if let Some(ref access_key) = tempo_access_key {
203                    let signer = signer.as_ref().expect("signer required for access key");
204                    tx.set_from(access_key.wallet_address);
205                    tx.set_key_id(access_key.key_address);
206
207                    let raw_tx = tx
208                        .sign_with_access_key(
209                            &provider,
210                            signer,
211                            access_key.wallet_address,
212                            access_key.key_address,
213                            access_key.key_authorization.as_ref(),
214                        )
215                        .await?;
216
217                    let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash();
218                    let cast = CastTxSender::new(&provider);
219                    cast.print_tx_result(
220                        tx_hash,
221                        send_tx.cast_async,
222                        send_tx.confirmations,
223                        timeout,
224                    )
225                    .await?
226                } else {
227                    let signer = signer.unwrap_or(send_tx.eth.wallet.signer().await?);
228                    let provider = build_provider_with_signer::<TempoNetwork>(&send_tx, signer)?;
229                    cast_send(
230                        provider,
231                        tx,
232                        send_tx.cast_async,
233                        send_tx.sync,
234                        send_tx.confirmations,
235                        timeout,
236                    )
237                    .await?
238                }
239            }
240        };
241        Ok(())
242    }
243}