stylus_tools/core/project/
contract.rs

1// Copyright 2025, Offchain Labs, Inc.
2// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3
4use std::path::PathBuf;
5
6use alloy::{
7    primitives::{Address, TxHash, B256, U256},
8    providers::{Provider, WalletProvider},
9};
10use cargo_metadata::{semver::Version, Package, TargetKind};
11
12use crate::{
13    core::{
14        build::{build_contract, BuildConfig, BuildError},
15        check::{check_contract, CheckConfig, CheckError},
16        deployment::{deploy, DeploymentConfig, DeploymentError},
17        manifest,
18        reflection::ReflectionConfig,
19        verification::{self, VerificationStatus},
20    },
21    error::decode_contract_error,
22    ops,
23    precompiles::{self, ArbWasm::ArbWasmErrors},
24    utils::toolchain::get_toolchain_channel,
25};
26
27#[derive(Debug)]
28pub struct Contract {
29    // Metadata package
30    pub package: Package,
31
32    // Toolchain metadata
33    stable: bool,
34
35    // Cargo metadata
36    name: String,
37    version: Version,
38}
39
40impl Contract {
41    pub fn is_contract(package: &Package) -> bool {
42        if let Some(stylus_manifest_path) = package
43            .manifest_path
44            .parent()
45            .map(|p| p.join(manifest::FILENAME))
46        {
47            stylus_manifest_path.exists()
48        } else {
49            false
50        }
51    }
52
53    pub fn stable(&self) -> bool {
54        self.stable
55    }
56
57    pub fn name(&self) -> &str {
58        &self.name
59    }
60
61    pub fn version(&self) -> &Version {
62        &self.version
63    }
64
65    /// Checks whether a contract has already been activated with the most recent version of Stylus.
66    pub async fn exists(codehash: B256, provider: &impl Provider) -> Result<bool, ContractError> {
67        let arbwasm = precompiles::arb_wasm(provider);
68        match arbwasm.codehashVersion(codehash).call().await {
69            Ok(_) => Ok(true),
70            Err(e) => {
71                let errs = decode_contract_error(e)?;
72                use ArbWasmErrors as A;
73                match errs {
74                    A::ProgramNotActivated(_)
75                    | A::ProgramNeedsUpgrade(_)
76                    | A::ProgramExpired(_) => Ok(false),
77                    _ => Err(ContractError::UnexpectedArbWasmError),
78                }
79            }
80        }
81    }
82
83    pub fn build(&self, config: &BuildConfig) -> Result<PathBuf, BuildError> {
84        build_contract(self, config)
85    }
86
87    pub async fn check(
88        &self,
89        address: Option<Address>,
90        config: &CheckConfig,
91        provider: &impl Provider,
92    ) -> Result<ContractStatus, CheckError> {
93        check_contract(self, address, config, provider).await
94    }
95
96    pub async fn deploy(
97        &self,
98        config: &DeploymentConfig,
99        provider: &(impl Provider + WalletProvider),
100    ) -> Result<(), DeploymentError> {
101        deploy(self, config, provider).await
102    }
103
104    pub fn export_abi(&self, config: &ReflectionConfig) -> eyre::Result<()> {
105        ops::export_abi(self.package.name.as_ref(), config)
106    }
107
108    pub async fn verify(
109        &self,
110        tx_hash: TxHash,
111        skip_clean: bool,
112        provider: &impl Provider,
113    ) -> eyre::Result<VerificationStatus> {
114        let status = verification::verify(self, tx_hash, skip_clean, provider).await?;
115        Ok(status)
116    }
117
118    pub fn print_constructor(&self, config: &ReflectionConfig) -> eyre::Result<()> {
119        ops::print_constructor(self.package.name.as_ref(), config)
120    }
121}
122
123impl TryFrom<&Package> for Contract {
124    type Error = ContractError;
125
126    fn try_from(package: &Package) -> Result<Self, Self::Error> {
127        let toolchain_channel = get_toolchain_channel(package)?;
128        let stable = !toolchain_channel.contains("nightly");
129        let version = package.version.clone();
130        // First, let's try to find if the library's name is set, since this will interfere with
131        // finding the wasm file in the deps directory if it's different.
132        let name = package
133            .targets
134            .iter()
135            .find_map(|t| t.kind.contains(&TargetKind::Lib).then(|| t.name.clone()))
136            // If that doesn't work, then we can use the package name, and break normally.
137            .unwrap_or_else(|| package.name.to_string());
138        Ok(Self {
139            package: package.clone(),
140            stable,
141            version,
142            name,
143        })
144    }
145}
146
147#[derive(Debug, thiserror::Error)]
148pub enum ContractError {
149    #[error("{0}")]
150    ContractDecode(#[from] crate::error::ContractDecodeError),
151    #[error("{0}")]
152    Toolchain(#[from] crate::utils::toolchain::ToolchainError),
153
154    #[error("unexpected ArbWasm error")]
155    UnexpectedArbWasmError,
156}
157
158#[derive(Debug)]
159pub enum ContractStatus {
160    /// Contract already exists onchain.
161    Active { code: Vec<u8> },
162    /// Contract can be activated with the given data fee.
163    Ready { code: Vec<u8>, fee: U256 },
164}
165
166impl ContractStatus {
167    pub fn code(&self) -> &[u8] {
168        match self {
169            Self::Active { code } => code,
170            Self::Ready { code, .. } => code,
171        }
172    }
173
174    pub fn suggest_fee(&self) -> U256 {
175        match self {
176            Self::Active { .. } => U256::ZERO,
177            Self::Ready { fee, .. } => *fee,
178        }
179    }
180}