o2-tools 0.4.8-rc3

Reusable tooling for trade account and order book contract interactions on Fuel
Documentation
//! Shared SRC-14 blob proxy bindings and deployment. Implementations initialize separately.
use anyhow::{
    Context,
    Result,
    ensure,
};
use fuels::{
    core::Configurables,
    prelude::*,
    tx::StorageSlot,
    types::Identity,
};

abigen!(Contract(
    name = "GenericProxyContract",
    abi = "artifacts/generic-proxy/generic-proxy-abi.json"
));
pub const GENERIC_PROXY_BYTECODE: &[u8] =
    include_bytes!("../artifacts/generic-proxy/generic-proxy.bin");
pub const GENERIC_PROXY_STORAGE: &[u8] =
    include_bytes!("../artifacts/generic-proxy/generic-proxy-storage_slots.json");
#[derive(Clone, Debug)]
pub struct GenericProxyDeployConfig {
    /// Persist and reuse this ID across releases, even if proxy derivation changes.
    pub contract_id: Option<ContractId>,
    pub salt: Salt,
    pub upgrade_bytecode: bool,
    pub max_words_per_blob: usize,
}

impl Default for GenericProxyDeployConfig {
    fn default() -> Self {
        Self {
            contract_id: None,
            salt: Salt::zeroed(),
            upgrade_bytecode: false,
            max_words_per_blob: 10_000,
        }
    }
}

pub struct GenericProxyDeployment<W: Account> {
    pub proxy: GenericProxyContract<W>,
    pub contract_id: ContractId,
    pub blob_id: ContractId,
}
impl<W: Account + Clone> GenericProxyDeployment<W> {
    pub async fn deploy(
        wallet: &W,
        config: &GenericProxyDeployConfig,
        implementation_bytecode: &[u8],
        implementation_storage_json: &[u8],
    ) -> Result<Self> {
        ensure!(
            config.max_words_per_blob > 0,
            "GenericProxy loader blob size cannot be zero"
        );
        let provider = wallet.try_provider()?;
        if let Some(id) = config.contract_id {
            ensure!(
                id != ContractId::zeroed() && provider.contract_exists(&id).await?,
                "configured generic proxy does not exist: {id}"
            );
        }
        let implementation_storage: Vec<StorageSlot> =
            serde_json::from_slice(implementation_storage_json)?;
        let (data, loader) = crate::blob_loader::build_loader_blobs(
            implementation_bytecode.to_vec(),
            config.salt,
            implementation_storage.clone(),
            Configurables::default(),
            config.max_words_per_blob,
        )?;
        let desired: ContractId = loader.id().into();
        let id = match config.contract_id {
            Some(id) => id,
            None => {
                crate::blob_loader::upload_loader_blobs(
                    wallet,
                    data.clone(),
                    loader.clone(),
                )
                .await?;
                let mut slots: Vec<StorageSlot> =
                    serde_json::from_slice(GENERIC_PROXY_STORAGE)?;
                for slot in implementation_storage {
                    ensure!(
                        !slots.iter().any(|s| s.key() == slot.key()),
                        "generic proxy/implementation storage collision"
                    );
                    slots.push(slot);
                }
                let contract = Contract::regular(
                    GENERIC_PROXY_BYTECODE.to_vec(),
                    config.salt,
                    slots,
                )
                .with_configurables(
                    GenericProxyContractConfigurables::default()
                        .with_INITIAL_OWNER(State::Initialized(Identity::Address(
                            wallet.address(),
                        )))?
                        .with_INITIAL_TARGET(desired)?,
                );
                let id = contract.contract_id();
                if !provider.contract_exists(&id).await? {
                    contract.deploy(wallet, TxPolicies::default()).await?;
                }
                id
            }
        };
        let proxy = GenericProxyContract::new(id, wallet.clone());
        let mut target = proxy
            .methods()
            .proxy_target()
            .simulate(Execution::state_read_only())
            .await?
            .value;
        if target.is_none() {
            proxy
                .methods()
                .initialize_proxy()
                .call()
                .await
                .context("initialize generic proxy")?;
            target = proxy
                .methods()
                .proxy_target()
                .simulate(Execution::state_read_only())
                .await?
                .value;
        }
        if config.upgrade_bytecode && target != Some(desired) {
            crate::blob_loader::upload_loader_blobs(wallet, data, loader).await?;
            proxy
                .methods()
                .set_proxy_target(desired)
                .call()
                .await
                .context("upgrade GenericProxy implementation")?;
            target = Some(desired);
        }
        Ok(Self {
            proxy,
            contract_id: id,
            blob_id: target.context("proxy has no target")?,
        })
    }
}