use std::collections::BTreeMap;
use std::time::Duration;
use alloy::primitives::{Address, B256, U256, keccak256};
use crate::artifacts::{ContractArtifact, ParalockContract};
use crate::bindings::{ITIP20Factory, ITIP20Issuer, MockUSD};
use crate::config::{EvmConfig, EvmSigner};
use crate::connection::{EvmConnection, TxRequest};
use crate::error::{EvmError, RevertSource};
use crate::networks::{ParalockDeployment, known_network};
use crate::tempo::{TEMPO_TOKENS, TIP20_FACTORY, is_tempo_chain_id};
pub const LOCAL_MINT_AMOUNT: U256 = U256::from_limbs([1_000_000_000_000_000, 0, 0, 0]);
pub fn local_ousd_salt() -> B256 {
keccak256("paralock/ousd")
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PreviousDeployment {
pub htlc: Option<Address>,
pub adaptor_escrow: Option<Address>,
pub start_block: Option<u64>,
pub tokens: BTreeMap<String, Address>,
}
impl From<&ParalockDeployment> for PreviousDeployment {
fn from(d: &ParalockDeployment) -> Self {
Self {
htlc: Some(d.htlc),
adaptor_escrow: Some(d.adaptor_escrow),
start_block: Some(d.start_block),
tokens: d.tokens.clone(),
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct DeployOptions {
pub rpc_url: String,
pub signer: EvmSigner,
pub chain_id: Option<u64>,
pub chain: Option<String>,
pub tempo: Option<bool>,
pub fee_token: Option<Address>,
pub previous: PreviousDeployment,
pub mint_to: Option<Vec<Address>>,
pub mint_amount: U256,
pub receipt_timeout: Duration,
}
impl DeployOptions {
pub fn new(rpc_url: impl Into<String>, signer: impl Into<EvmSigner>) -> Self {
Self {
rpc_url: rpc_url.into(),
signer: signer.into(),
chain_id: None,
chain: None,
tempo: None,
fee_token: None,
previous: PreviousDeployment::default(),
mint_to: None,
mint_amount: LOCAL_MINT_AMOUNT,
receipt_timeout: Duration::from_secs(180),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Reused {
pub htlc: bool,
pub adaptor_escrow: bool,
pub quote_token: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeployResult {
pub deployment: ParalockDeployment,
pub reused: Reused,
}
pub async fn deploy_paralock_contracts(options: DeployOptions) -> Result<DeployResult, EvmError> {
let chain_id = match options.chain_id {
Some(id) => id,
None => crate::connection::fetch_chain_id(&options.rpc_url).await?,
};
if let Some(known) = known_network(chain_id) {
return Err(EvmError::PublicNetwork { chain_id, name: known.name });
}
let tempo = options.tempo.unwrap_or(options.fee_token.is_some() || is_tempo_chain_id(chain_id));
let mut config = EvmConfig::new(options.rpc_url.clone(), chain_id)
.with_signer(options.signer.clone())
.with_tempo(tempo)
.with_receipt_timeout(options.receipt_timeout);
config.fee_token = options.fee_token;
let conn = EvmConnection::connect(config).await?;
let deployer = options.signer.address();
let previous = &options.previous;
let mint_to = options.mint_to.clone().unwrap_or_else(|| vec![deployer]);
let htlc_artifact = ParalockContract::Htlc.artifact()?;
let escrow_artifact = ParalockContract::AdaptorEscrow.artifact()?;
let mut reused = Reused {
htlc: is_current(&conn, previous.htlc, &htlc_artifact).await?,
adaptor_escrow: is_current(&conn, previous.adaptor_escrow, &escrow_artifact).await?,
quote_token: false,
};
let start_block = match previous.start_block {
Some(start) if reused.htlc || reused.adaptor_escrow => start,
_ => conn.block_number().await?,
};
let htlc = match previous.htlc {
Some(address) if reused.htlc => {
tracing::info!(%address, "reusing ParalockHTLC");
address
}
_ => deploy(&conn, &htlc_artifact, true).await?,
};
let adaptor_escrow = match previous.adaptor_escrow {
Some(address) if reused.adaptor_escrow => {
tracing::info!(%address, "reusing ParalockAdaptorEscrow");
address
}
_ => deploy(&conn, &escrow_artifact, true).await?,
};
let mut tokens = BTreeMap::new();
let quote_token = if tempo {
let salt = local_ousd_salt();
let token: Address = conn
.read(
TIP20_FACTORY,
&ITIP20Factory::getTokenAddressCall { sender: deployer, salt },
None,
Some(RevertSource::Token),
)
.await?;
reused.quote_token =
conn.read(TIP20_FACTORY, &ITIP20Factory::isTIP20Call { token }, None, Some(RevertSource::Token)).await?;
if reused.quote_token {
tracing::info!(%token, "reusing the local OUSD stand-in");
} else {
let create = ITIP20Factory::createTokenCall {
name: "Open USD (Paralock local)".to_owned(),
symbol: "OUSD".to_owned(),
currency: "USD".to_owned(),
quoteToken: TEMPO_TOKENS.path_usd,
admin: deployer,
salt,
};
conn.send_and_confirm(TxRequest::call(TIP20_FACTORY, &create, Some(RevertSource::Token))).await?;
let issuer: B256 =
conn.read(token, &ITIP20Issuer::ISSUER_ROLECall {}, None, Some(RevertSource::Token)).await?;
let grant = ITIP20Issuer::grantRoleCall { role: issuer, account: deployer };
conn.send_and_confirm(TxRequest::call(token, &grant, Some(RevertSource::Token))).await?;
for to in &mint_to {
let mint = ITIP20Issuer::mintCall { to: *to, amount: options.mint_amount };
conn.send_and_confirm(TxRequest::call(token, &mint, Some(RevertSource::Token))).await?;
}
tracing::info!(%token, "created the local OUSD stand-in");
}
tokens.insert("AlphaUSD".to_owned(), TEMPO_TOKENS.alpha_usd);
tokens.insert("BetaUSD".to_owned(), TEMPO_TOKENS.beta_usd);
tokens.insert("OUSD".to_owned(), token);
tokens.insert("PathUSD".to_owned(), TEMPO_TOKENS.path_usd);
tokens.insert("ThetaUSD".to_owned(), TEMPO_TOKENS.theta_usd);
"OUSD"
} else {
let previous_usdc = previous.tokens.get("USDC").copied();
reused.quote_token = is_own_mock_usd(&conn, previous_usdc, deployer).await?;
let token = match previous_usdc {
Some(token) if reused.quote_token => {
tracing::info!(%token, "reusing MockUSD");
token
}
_ => {
let token = deploy(&conn, &ParalockContract::MockUsd.artifact()?, false).await?;
for to in &mint_to {
let mint = MockUSD::mintCall { to: *to, amount: options.mint_amount };
conn.send_and_confirm(TxRequest::call(token, &mint, Some(RevertSource::Token))).await?;
}
token
}
};
tokens.insert("USDC".to_owned(), token);
"USDC"
};
let chain = options.chain.clone().or_else(|| tempo.then(|| "tempo".to_owned()));
let deployment = ParalockDeployment {
adaptor_escrow,
chain,
chain_id,
deployer,
htlc,
quote_token: quote_token.to_owned(),
start_block,
tokens,
};
Ok(DeployResult { deployment, reused })
}
async fn deploy(conn: &EvmConnection, artifact: &ContractArtifact, check_code: bool) -> Result<Address, EvmError> {
let result = conn.send_and_confirm(TxRequest::deploy(artifact.bytecode.clone())).await?;
let address = result.contract_address.ok_or_else(|| {
EvmError::InvalidResponse(format!("{} deployment {} created no contract", artifact.contract_name, result.hash))
})?;
if check_code && !conn.holds_code(address, &artifact.deployed_bytecode).await? {
return Err(EvmError::InvalidResponse(format!(
"{} at {address} does not hold the expected runtime code",
artifact.contract_name
)));
}
tracing::info!(contract = %artifact.contract_name, %address, "deployed");
Ok(address)
}
async fn is_current(
conn: &EvmConnection,
address: Option<Address>,
artifact: &ContractArtifact,
) -> Result<bool, EvmError> {
match address {
Some(address) => conn.holds_code(address, &artifact.deployed_bytecode).await,
None => Ok(false),
}
}
async fn is_own_mock_usd(conn: &EvmConnection, address: Option<Address>, deployer: Address) -> Result<bool, EvmError> {
let Some(address) = address else { return Ok(false) };
if conn.code_at(address).await?.is_empty() {
return Ok(false);
}
match conn.read(address, &MockUSD::minterCall {}, None, Some(RevertSource::Token)).await {
Ok(minter) => Ok(minter == deployer),
Err(EvmError::TransactionReverted { .. } | EvmError::InvalidResponse(_)) => Ok(false),
Err(err) => Err(err),
}
}