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
34pub(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#[derive(Debug, Parser, Clone)]
59pub enum Tip20Subcommand {
60 #[command(visible_alias = "c")]
62 Create {
63 name: String,
65
66 symbol: String,
68
69 currency: String,
73
74 #[arg(value_parser = NameOrAddress::from_str)]
76 quote_token: NameOrAddress,
77
78 #[arg(value_parser = NameOrAddress::from_str)]
80 admin: NameOrAddress,
81
82 salt: B256,
84
85 #[arg(long)]
87 force: bool,
88
89 #[command(flatten)]
90 send_tx: SendTxOpts,
91
92 #[command(flatten)]
93 tx: Tip20TxOpts,
94 },
95}
96
97#[derive(Debug, Clone, Args)]
99#[command(next_help_heading = "Transaction options")]
100pub struct Tip20TxOpts {
101 #[arg(long)]
103 pub gas_limit: Option<U256>,
104
105 #[arg(long)]
107 pub gas_price: Option<U256>,
108
109 #[arg(long)]
111 pub priority_gas_price: Option<U256>,
112
113 #[arg(long)]
115 pub nonce: Option<U256>,
116
117 #[command(flatten)]
118 pub tempo: TempoOpts,
119}
120
121impl Tip20TxOpts {
122 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(¤cy) && !force {
180 sh_warn!("{}", iso4217_warning_message(¤cy))?;
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}