1use std::collections::BTreeMap;
4use std::time::Duration;
5
6use alloy::primitives::{Address, B256, U256, keccak256};
7
8use crate::artifacts::{ContractArtifact, ParalockContract};
9use crate::bindings::{ITIP20Factory, ITIP20Issuer, MockUSD};
10use crate::config::{EvmConfig, EvmSigner};
11use crate::connection::{EvmConnection, TxRequest};
12use crate::error::{EvmError, RevertSource};
13use crate::networks::{ParalockDeployment, known_network};
14use crate::tempo::{TEMPO_TOKENS, TIP20_FACTORY, is_tempo_chain_id};
15
16pub const LOCAL_MINT_AMOUNT: U256 = U256::from_limbs([1_000_000_000_000_000, 0, 0, 0]);
18
19pub fn local_ousd_salt() -> B256 {
21 keccak256("paralock/ousd")
22}
23
24#[derive(Clone, Debug, Default, PartialEq, Eq)]
26pub struct PreviousDeployment {
27 pub htlc: Option<Address>,
29 pub adaptor_escrow: Option<Address>,
31 pub start_block: Option<u64>,
33 pub tokens: BTreeMap<String, Address>,
35}
36
37impl From<&ParalockDeployment> for PreviousDeployment {
38 fn from(d: &ParalockDeployment) -> Self {
39 Self {
40 htlc: Some(d.htlc),
41 adaptor_escrow: Some(d.adaptor_escrow),
42 start_block: Some(d.start_block),
43 tokens: d.tokens.clone(),
44 }
45 }
46}
47
48#[derive(Clone, Debug)]
50#[non_exhaustive]
51pub struct DeployOptions {
52 pub rpc_url: String,
54 pub signer: EvmSigner,
56 pub chain_id: Option<u64>,
58 pub chain: Option<String>,
60 pub tempo: Option<bool>,
62 pub fee_token: Option<Address>,
64 pub previous: PreviousDeployment,
66 pub mint_to: Option<Vec<Address>>,
68 pub mint_amount: U256,
70 pub receipt_timeout: Duration,
72}
73
74impl DeployOptions {
75 pub fn new(rpc_url: impl Into<String>, signer: impl Into<EvmSigner>) -> Self {
77 Self {
78 rpc_url: rpc_url.into(),
79 signer: signer.into(),
80 chain_id: None,
81 chain: None,
82 tempo: None,
83 fee_token: None,
84 previous: PreviousDeployment::default(),
85 mint_to: None,
86 mint_amount: LOCAL_MINT_AMOUNT,
87 receipt_timeout: Duration::from_secs(180),
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
94pub struct Reused {
95 pub htlc: bool,
97 pub adaptor_escrow: bool,
99 pub quote_token: bool,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct DeployResult {
106 pub deployment: ParalockDeployment,
108 pub reused: Reused,
110}
111
112pub async fn deploy_paralock_contracts(options: DeployOptions) -> Result<DeployResult, EvmError> {
122 let chain_id = match options.chain_id {
123 Some(id) => id,
124 None => crate::connection::fetch_chain_id(&options.rpc_url).await?,
125 };
126 if let Some(known) = known_network(chain_id) {
127 return Err(EvmError::PublicNetwork { chain_id, name: known.name });
128 }
129 let tempo = options.tempo.unwrap_or(options.fee_token.is_some() || is_tempo_chain_id(chain_id));
130 let mut config = EvmConfig::new(options.rpc_url.clone(), chain_id)
131 .with_signer(options.signer.clone())
132 .with_tempo(tempo)
133 .with_receipt_timeout(options.receipt_timeout);
134 config.fee_token = options.fee_token;
135 let conn = EvmConnection::connect(config).await?;
136 let deployer = options.signer.address();
137 let previous = &options.previous;
138 let mint_to = options.mint_to.clone().unwrap_or_else(|| vec![deployer]);
139
140 let htlc_artifact = ParalockContract::Htlc.artifact()?;
141 let escrow_artifact = ParalockContract::AdaptorEscrow.artifact()?;
142 let mut reused = Reused {
143 htlc: is_current(&conn, previous.htlc, &htlc_artifact).await?,
144 adaptor_escrow: is_current(&conn, previous.adaptor_escrow, &escrow_artifact).await?,
145 quote_token: false,
146 };
147 let start_block = match previous.start_block {
149 Some(start) if reused.htlc || reused.adaptor_escrow => start,
150 _ => conn.block_number().await?,
151 };
152
153 let htlc = match previous.htlc {
154 Some(address) if reused.htlc => {
155 tracing::info!(%address, "reusing ParalockHTLC");
156 address
157 }
158 _ => deploy(&conn, &htlc_artifact, true).await?,
159 };
160 let adaptor_escrow = match previous.adaptor_escrow {
161 Some(address) if reused.adaptor_escrow => {
162 tracing::info!(%address, "reusing ParalockAdaptorEscrow");
163 address
164 }
165 _ => deploy(&conn, &escrow_artifact, true).await?,
166 };
167
168 let mut tokens = BTreeMap::new();
169 let quote_token = if tempo {
170 let salt = local_ousd_salt();
171 let token: Address = conn
172 .read(
173 TIP20_FACTORY,
174 &ITIP20Factory::getTokenAddressCall { sender: deployer, salt },
175 None,
176 Some(RevertSource::Token),
177 )
178 .await?;
179 reused.quote_token =
180 conn.read(TIP20_FACTORY, &ITIP20Factory::isTIP20Call { token }, None, Some(RevertSource::Token)).await?;
181 if reused.quote_token {
182 tracing::info!(%token, "reusing the local OUSD stand-in");
183 } else {
184 let create = ITIP20Factory::createTokenCall {
185 name: "Open USD (Paralock local)".to_owned(),
186 symbol: "OUSD".to_owned(),
187 currency: "USD".to_owned(),
188 quoteToken: TEMPO_TOKENS.path_usd,
189 admin: deployer,
190 salt,
191 };
192 conn.send_and_confirm(TxRequest::call(TIP20_FACTORY, &create, Some(RevertSource::Token))).await?;
193 let issuer: B256 =
194 conn.read(token, &ITIP20Issuer::ISSUER_ROLECall {}, None, Some(RevertSource::Token)).await?;
195 let grant = ITIP20Issuer::grantRoleCall { role: issuer, account: deployer };
196 conn.send_and_confirm(TxRequest::call(token, &grant, Some(RevertSource::Token))).await?;
197 for to in &mint_to {
198 let mint = ITIP20Issuer::mintCall { to: *to, amount: options.mint_amount };
199 conn.send_and_confirm(TxRequest::call(token, &mint, Some(RevertSource::Token))).await?;
200 }
201 tracing::info!(%token, "created the local OUSD stand-in");
202 }
203 tokens.insert("AlphaUSD".to_owned(), TEMPO_TOKENS.alpha_usd);
204 tokens.insert("BetaUSD".to_owned(), TEMPO_TOKENS.beta_usd);
205 tokens.insert("OUSD".to_owned(), token);
206 tokens.insert("PathUSD".to_owned(), TEMPO_TOKENS.path_usd);
207 tokens.insert("ThetaUSD".to_owned(), TEMPO_TOKENS.theta_usd);
208 "OUSD"
209 } else {
210 let previous_usdc = previous.tokens.get("USDC").copied();
211 reused.quote_token = is_own_mock_usd(&conn, previous_usdc, deployer).await?;
212 let token = match previous_usdc {
213 Some(token) if reused.quote_token => {
214 tracing::info!(%token, "reusing MockUSD");
215 token
216 }
217 _ => {
218 let token = deploy(&conn, &ParalockContract::MockUsd.artifact()?, false).await?;
219 for to in &mint_to {
220 let mint = MockUSD::mintCall { to: *to, amount: options.mint_amount };
221 conn.send_and_confirm(TxRequest::call(token, &mint, Some(RevertSource::Token))).await?;
222 }
223 token
224 }
225 };
226 tokens.insert("USDC".to_owned(), token);
227 "USDC"
228 };
229
230 let chain = options.chain.clone().or_else(|| tempo.then(|| "tempo".to_owned()));
231 let deployment = ParalockDeployment {
232 adaptor_escrow,
233 chain,
234 chain_id,
235 deployer,
236 htlc,
237 quote_token: quote_token.to_owned(),
238 start_block,
239 tokens,
240 };
241 Ok(DeployResult { deployment, reused })
242}
243
244async fn deploy(conn: &EvmConnection, artifact: &ContractArtifact, check_code: bool) -> Result<Address, EvmError> {
246 let result = conn.send_and_confirm(TxRequest::deploy(artifact.bytecode.clone())).await?;
247 let address = result.contract_address.ok_or_else(|| {
248 EvmError::InvalidResponse(format!("{} deployment {} created no contract", artifact.contract_name, result.hash))
249 })?;
250 if check_code && !conn.holds_code(address, &artifact.deployed_bytecode).await? {
251 return Err(EvmError::InvalidResponse(format!(
252 "{} at {address} does not hold the expected runtime code",
253 artifact.contract_name
254 )));
255 }
256 tracing::info!(contract = %artifact.contract_name, %address, "deployed");
257 Ok(address)
258}
259
260async fn is_current(
262 conn: &EvmConnection,
263 address: Option<Address>,
264 artifact: &ContractArtifact,
265) -> Result<bool, EvmError> {
266 match address {
267 Some(address) => conn.holds_code(address, &artifact.deployed_bytecode).await,
268 None => Ok(false),
269 }
270}
271
272async fn is_own_mock_usd(conn: &EvmConnection, address: Option<Address>, deployer: Address) -> Result<bool, EvmError> {
274 let Some(address) = address else { return Ok(false) };
275 if conn.code_at(address).await?.is_empty() {
276 return Ok(false);
277 }
278 match conn.read(address, &MockUSD::minterCall {}, None, Some(RevertSource::Token)).await {
279 Ok(minter) => Ok(minter == deployer),
280 Err(EvmError::TransactionReverted { .. } | EvmError::InvalidResponse(_)) => Ok(false),
281 Err(err) => Err(err),
282 }
283}