diff --git a/.cargo/config.toml b/.cargo/config.toml index 6bb550b..db369d8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -25,6 +25,7 @@ rustflags = [ "-Aclippy::must_use_candidate", "-Aclippy::missing_panics_doc", "-Aclippy::missing_errors_doc", + "-Aclippy::module-name-repetitions", # "-Aclippy::missing_safety_doc", # "-Aclippy::inline_always", # "-Aclippy::default_trait_access", diff --git a/Cargo.lock b/Cargo.lock index 50ee87b..e7a5882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,14 +364,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0198b9d0078e0f30dedc7acbb21c974e838fc8fae3ee170128658a98cb2c1c04" -[[package]] -name = "fetch-redeploy" -version = "0.0.0" -dependencies = [ - "loam-sdk", - "loam-sdk-core-riffs", -] - [[package]] name = "fnv" version = "1.0.7" @@ -526,7 +518,7 @@ dependencies = [ [[package]] name = "loam-sdk" version = "0.1.0" -source = "git+https://github.com/loambuild/loam-sdk?rev=fb45acf5cb508f5ffc85c335221b56aa10842897#fb45acf5cb508f5ffc85c335221b56aa10842897" +source = "git+https://github.com/loambuild/loam-sdk?tag=v0.1.0#33fc1afbf7bf7b26c8d7f95e8b0f40d5581a1b0f" dependencies = [ "loam-soroban-sdk", "macro-wrapper", @@ -535,7 +527,7 @@ dependencies = [ [[package]] name = "loam-sdk-core-riffs" version = "0.1.0" -source = "git+https://github.com/loambuild/loam-sdk?rev=fb45acf5cb508f5ffc85c335221b56aa10842897#fb45acf5cb508f5ffc85c335221b56aa10842897" +source = "git+https://github.com/loambuild/loam-sdk?tag=v0.1.0#33fc1afbf7bf7b26c8d7f95e8b0f40d5581a1b0f" dependencies = [ "loam-sdk", ] @@ -543,7 +535,7 @@ dependencies = [ [[package]] name = "loam-soroban-sdk" version = "0.1.0" -source = "git+https://github.com/loambuild/loam-sdk?rev=fb45acf5cb508f5ffc85c335221b56aa10842897#fb45acf5cb508f5ffc85c335221b56aa10842897" +source = "git+https://github.com/loambuild/loam-sdk?tag=v0.1.0#33fc1afbf7bf7b26c8d7f95e8b0f40d5581a1b0f" dependencies = [ "macro-wrapper", "soroban-sdk", @@ -561,7 +553,7 @@ dependencies = [ [[package]] name = "macro-wrapper" version = "0.0.1" -source = "git+https://github.com/loambuild/loam-sdk?rev=fb45acf5cb508f5ffc85c335221b56aa10842897#fb45acf5cb508f5ffc85c335221b56aa10842897" +source = "git+https://github.com/loambuild/loam-sdk?tag=v0.1.0#33fc1afbf7bf7b26c8d7f95e8b0f40d5581a1b0f" dependencies = [ "Inflector", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index ec1d5dc..0ff7866 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,5 +27,5 @@ lto = true [workspace.dependencies] # loam-sdk = { path = "../../loam/crates/loam" } # loam-sdk-core-riffs = { path = "../../loam/crates/loam-core" } -loam-sdk = { git = "https://github.com/loambuild/loam-sdk", rev = "fb45acf5cb508f5ffc85c335221b56aa10842897" } -loam-sdk-core-riffs = { git = "https://github.com/loambuild/loam-sdk", rev = "fb45acf5cb508f5ffc85c335221b56aa10842897" } +loam-sdk = { git = "https://github.com/loambuild/loam-sdk", tag = "v0.1.0" } +loam-sdk-core-riffs = { git = "https://github.com/loambuild/loam-sdk", tag = "v0.1.0" } diff --git a/contract_id.txt b/contract_id.txt index 704e05d..6d13b00 100644 --- a/contract_id.txt +++ b/contract_id.txt @@ -1 +1 @@ -8f20288b65636659d758493805f798bb623d1a8fc83eb7dfcfefb3b39e3f0f7d \ No newline at end of file +f0979039df1471f9f7ecc0b55bbc49f57f26e7035fd42fba97d23ad024df4df4 \ No newline at end of file diff --git a/crates/smartdeploy/Cargo.toml b/crates/smartdeploy/Cargo.toml index 5df36d5..fe6a98c 100644 --- a/crates/smartdeploy/Cargo.toml +++ b/crates/smartdeploy/Cargo.toml @@ -1,5 +1,7 @@ [package] name = "smartdeploy" +description = "A crate for managing and deploying smart contracts on the Soroban blockchain." +documentation = "https://docs.rs/smartdeploy" version = "0.0.0" authors = ["Stellar Development Foundation "] license = "Apache-2.0" diff --git a/crates/smartdeploy/README.md b/crates/smartdeploy/README.md new file mode 100644 index 0000000..33bf8cb --- /dev/null +++ b/crates/smartdeploy/README.md @@ -0,0 +1,20 @@ +# SmartDeploy + +`smartdeploy` is a Rust crate for managing and deploying smart contracts on the Soroban blockchain. It provides an easy-to-use interface for developers to interact with the blockchain and deploy their smart contracts without dealing with low-level implementation details. + +## Features + +- Register contract names for publishing +- Publish contract binaries with version management +- Fetch contract binaries and metadata +- Deploy published contracts to the blockchain +- Retrieve deployment statistics for contracts +- Manage contract ownership and redeployment + +## Usage + +Add `smartdeploy` as a dependency in your project's `Cargo.toml` file: + +```toml +[dependencies] +smartdeploy = "0.0.0" diff --git a/crates/smartdeploy/src/gen.rs b/crates/smartdeploy/src/gen.rs index 3eb717b..e6acd50 100644 --- a/crates/smartdeploy/src/gen.rs +++ b/crates/smartdeploy/src/gen.rs @@ -1,40 +1,34 @@ #![allow(clippy::let_unit_value)] -use loam_sdk::soroban_sdk::{self, contractimpl, set_env, Address, BytesN, Env, Lazy, String}; +use loam_sdk::soroban_sdk::{self, contractimpl, set_env, Address, BytesN, Env, String}; use loam_sdk_core_riffs::{Ownable, Redeployable}; -use crate::{error::Error, version::Version, ContractMetadata, SmartDeploy}; +use crate::{ + error::Error, + metadata::PublishedWasm, + registry::{Binary, Deployable}, + version::Version, + Contract, +}; pub struct SorobanContract; #[contractimpl] impl SorobanContract { - /// Register a contract name to allow publishing. - - pub fn register_name(env: Env, author: Address, contract_name: String) -> Result<(), Error> { - set_env(env); - let mut this = SmartDeploy::get_lazy().unwrap_or_default(); - let res = this.register_name(author, contract_name)?; - SmartDeploy::set_lazy(this); - Ok(res) - } - /// Publish a contract. /// Currently a contract's version is a `u32` and publishing will increment it. /// If no repo is provided, then the previously published binary's repo will be used. If it's the first /// time then it will be empty. /// `kind` is Patch by default, - pub fn publish_binary( + pub fn publish( env: Env, contract_name: String, + author: Address, hash: BytesN<32>, repo: Option, kind: Option, ) -> Result<(), Error> { set_env(env); - let mut this = SmartDeploy::get_lazy().unwrap_or_default(); - let res = this.publish_binary(contract_name, hash, repo, kind)?; - SmartDeploy::set_lazy(this); - Ok(res) + Contract::publish(contract_name, author, hash, repo, kind) } /// Fetch the hash for a given `contract_name`. @@ -43,26 +37,23 @@ impl SorobanContract { env: Env, contract_name: String, version: Option, - ) -> Result, Error> { + ) -> Result { set_env(env); - SmartDeploy::get_lazy() - .unwrap_or_default() - .fetch(contract_name, version) + Contract::fetch(contract_name, version) } /// Fetch metadata for a given `contract_name`. /// If version is not provided, it is the most recent version. - pub fn fetch_metadata( + pub fn fetch_hash( env: Env, contract_name: String, version: Option, - ) -> Result { + ) -> Result, Error> { set_env(env); - SmartDeploy::get_lazy() - .unwrap_or_default() - .fetch_metadata(contract_name, version) + Contract::fetch_hash(contract_name, version) } + /// Deploy a contract and register a deployed contract pub fn deploy( env: Env, contract_name: String, @@ -71,38 +62,23 @@ impl SorobanContract { salt: Option>, ) -> Result, Error> { set_env(env); - let mut this = SmartDeploy::get_lazy().unwrap_or_default(); - let res = this.deploy(contract_name, version, deployed_name, salt)?; - SmartDeploy::set_lazy(this); - Ok(res) - } - - /// How many deploys have been made for the given contract. - pub fn get_num_deploys( - env: Env, - contract_name: String, - version: Option, - ) -> Result { - set_env(env); - SmartDeploy::get_lazy() - .unwrap_or_default() - .get_num_deploys(contract_name, version) + Contract::deploy(contract_name, version, deployed_name, salt) } /// Initial method called when contract is deployed. After that only the owner can set the owner, e.i. transfer ownership pub fn owner_set(env: Env, owner: Address) { set_env(env); - SmartDeploy::owner_set(owner); + Contract::owner_set(owner); } /// Current owner of the contract pub fn owner_get(env: Env) -> Option
{ set_env(env); - SmartDeploy::owner_get() + Contract::owner_get() } /// Redeploy contract to given hash pub fn redeploy(env: Env, hash: BytesN<32>) { set_env(env); - SmartDeploy::redeploy(hash); + Contract::redeploy(hash); } } diff --git a/crates/smartdeploy/src/lib.rs b/crates/smartdeploy/src/lib.rs index 5206b0c..19ba3bd 100644 --- a/crates/smartdeploy/src/lib.rs +++ b/crates/smartdeploy/src/lib.rs @@ -1,261 +1,31 @@ #![no_std] -use loam_sdk::{ - soroban_sdk::{self, contracttype, get_env, log, Address, Bytes, BytesN, Env, Map, String}, - IntoKey, -}; +use loam_sdk_core_riffs::{owner::Owner, Ownable, Redeployable}; +use registry::{Binary, ContractRegistry, Deployable}; extern crate alloc; -use loam_sdk_core_riffs::{owner::Owner, Ownable, Redeployable}; -use version::{Version, INITAL_VERSION}; pub mod error; pub mod gen; +pub mod metadata; pub mod registry; +pub mod util; pub mod version; -use error::Error; - -#[contracttype] -#[derive(IntoKey, Default)] -pub struct SmartDeploy { - pub contracts: Contracts, -} - -#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] -#[contracttype] -pub struct ContractMetadata { - repo: String, -} - -impl Default for ContractMetadata { - fn default() -> Self { - Self { - repo: String::from_slice(get_env(), ""), - } - } -} - -/// Contains -#[contracttype] -#[derive(Clone)] -pub struct PublishedBinary { - hash: BytesN<32>, - metadata: ContractMetadata, - num_deployed: u64, -} - -/// Contains -#[contracttype] -#[derive(Clone)] -pub struct PublishedContract { - versions: Map, - author: Address, -} - -impl PublishedContract { - pub fn new(author: Address) -> Self { - Self { - author, - versions: Map::new(get_env()), - } - } -} - -impl PublishedContract { - pub fn most_recent_version(&self) -> Result { - self.versions - .keys() - .last() - .transpose() - .unwrap() - .ok_or(Error::NoSuchVersion) - } - - pub fn get(&self, version: Option) -> Result { - let version = if let Some(version) = version { - version - } else { - self.most_recent_version()? - }; - self.versions - .get(version) - .transpose() - .unwrap() - .ok_or(Error::NoSuchVersion) - } - - pub fn set(&mut self, version: Option, binary: PublishedBinary) -> Result<(), Error> { - let version = if let Some(version) = version { - version - } else { - self.most_recent_version()? - }; - self.versions.set(version, binary); - Ok(()) - } -} - -#[contracttype] -#[derive(Clone)] -pub struct Contracts(Map); - -impl Default for Contracts { - fn default() -> Self { - Self(Map::new(get_env())) - } -} - -impl Contracts { - pub fn find_contract(&self, name: String) -> Result { - self.0 - .get(name) - .transpose() - .unwrap() - .ok_or(Error::NoSuchContract) - } - - pub fn find_version( - &self, - name: String, - version: Option, - ) -> Result { - self.find_contract(name)?.get(version) - } - - pub fn set_contract(&mut self, name: String, contract: PublishedContract) { - self.0.set(name, contract); - } -} -// #[loam] - -impl SmartDeploy { - /// Register a contract name to allow publishing. - pub fn register_name(&mut self, author: Address, contract_name: String) -> Result<(), Error> { - if self.contracts.find_contract(contract_name.clone()).is_ok() { - return Err(Error::AlreadyRegistered); - } - self.contracts - .set_contract(contract_name, PublishedContract::new(author)); - Ok(()) - } - - /// Publish a contract. - /// Currently a contract's version is a `u32` and publishing will increment it. - /// If no repo is provided, then the previously published binary's repo will be used. If it's the first - /// time then it will be empty. - /// `kind` is Patch by default, - pub fn publish_binary( - &mut self, - contract_name: String, - hash: BytesN<32>, - repo: Option, - kind: Option, - ) -> Result<(), Error> { - let mut contract = self.contracts.find_contract(contract_name.clone())?; - contract.author.require_auth(); - let keys = contract.versions.keys(); - let last_version = keys.last().transpose().unwrap().unwrap_or_default(); - log!(get_env(), "{}", last_version); - let new_version = last_version.clone().update(&kind.unwrap_or_default()); - let metadata = if let Some(repo) = repo { - ContractMetadata { repo } - } else if new_version == INITAL_VERSION { - ContractMetadata::default() - } else { - contract.get(Some(last_version))?.metadata - }; - let published_binary = PublishedBinary { - hash, - metadata, - num_deployed: 0, - }; - contract.versions.set(new_version, published_binary); - self.contracts.set_contract(contract_name, contract); - Ok(()) - } - - /// Fetch the hash for a given `contract_name`. - /// If version is not provided, it is the most recent version. - pub fn fetch( - &self, - contract_name: String, - version: Option, - ) -> Result, Error> { - Ok(self.contracts.find_version(contract_name, version)?.hash) - } - - /// Fetch metadata for a given `contract_name`. - /// If version is not provided, it is the most recent version. - pub fn fetch_metadata( - &self, - contract_name: String, - version: Option, - ) -> Result { - Ok(self - .contracts - .find_version(contract_name, version)? - .metadata) - } - - /// Deploys a new published contract returning the deployed contract's id. - /// If no salt provided it will use the current sequence number. - pub fn deploy( - &mut self, - contract_name: String, - version: Option, - deployed_name: String, - salt: Option>, - ) -> Result, Error> { - let env = get_env(); - let wasm_hash = self.fetch(contract_name.clone(), version.clone())?; - let mut contract = self.contracts.find_contract(contract_name.clone())?; - let mut binary = self - .contracts - .find_version(contract_name.clone(), version.clone())?; - - let salt = salt.unwrap_or_else(|| hash_string(env, &deployed_name)); - binary.num_deployed += 1; - log!(env, "num_deployed {}", binary.num_deployed); - contract.set(version, binary)?; - self.contracts.set_contract(contract_name, contract); - - Ok(env - .deployer() - .with_current_contract(&salt) - .deploy(&wasm_hash)) - } - - /// How many deploys have been made for the given contract. - pub fn get_num_deploys( - &self, - contract_name: String, - version: Option, - ) -> Result { - let contracts = &self.contracts; - let binary: PublishedBinary = contracts.find_version(contract_name, version)?; - Ok(binary.num_deployed) - } +pub struct Contract; - pub fn get_contracts(&self) -> Contracts { - self.contracts.clone() - } +impl Binary for Contract { + type Impl = ContractRegistry; } -pub fn hash_string(env: &Env, s: &String) -> BytesN<32> { - let len = s.len() as usize; - let mut bytes = [0u8; 100]; - let bytes = &mut bytes[0..len]; - s.copy_into_slice(bytes); - let mut b = Bytes::new(env); - b.copy_from_slice(0, bytes); - env.crypto().sha256(&b) +impl Deployable for Contract { + type Impl = ContractRegistry; } -impl Ownable for SmartDeploy { +impl Ownable for Contract { type Impl = Owner; } -impl Redeployable for SmartDeploy {} +impl Redeployable for Contract {} #[cfg(test)] mod test; diff --git a/crates/smartdeploy/src/metadata.rs b/crates/smartdeploy/src/metadata.rs new file mode 100644 index 0000000..56d6611 --- /dev/null +++ b/crates/smartdeploy/src/metadata.rs @@ -0,0 +1,77 @@ +use loam_sdk::soroban_sdk::{self, contracttype, get_env, Address, BytesN, Map, String}; + +use crate::{error::Error, version::Version}; + +#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] +#[contracttype] +pub struct ContractMetadata { + pub repo: String, +} + +impl Default for ContractMetadata { + fn default() -> Self { + Self { + repo: String::from_slice(get_env(), ""), + } + } +} + +/// Contains +#[contracttype] +#[derive(Clone)] +pub struct PublishedWasm { + pub hash: BytesN<32>, + pub metadata: ContractMetadata, + pub num_deployed: u64, +} + +/// Contains +#[contracttype] +#[derive(Clone)] +pub struct PublishedContract { + pub versions: Map, + pub author: Address, +} + +impl PublishedContract { + pub fn new(author: Address) -> Self { + Self { + author, + versions: Map::new(get_env()), + } + } +} + +impl PublishedContract { + pub fn most_recent_version(&self) -> Result { + self.versions + .keys() + .last() + .transpose() + .unwrap() + .ok_or(Error::NoSuchVersion) + } + + pub fn get(&self, version: Option) -> Result { + let version = if let Some(version) = version { + version + } else { + self.most_recent_version()? + }; + self.versions + .get(version) + .transpose() + .unwrap() + .ok_or(Error::NoSuchVersion) + } + + pub fn set(&mut self, version: Option, binary: PublishedWasm) -> Result<(), Error> { + let version = if let Some(version) = version { + version + } else { + self.most_recent_version()? + }; + self.versions.set(version, binary); + Ok(()) + } +} diff --git a/crates/smartdeploy/src/registry.rs b/crates/smartdeploy/src/registry.rs index 2896b41..01726ca 100644 --- a/crates/smartdeploy/src/registry.rs +++ b/crates/smartdeploy/src/registry.rs @@ -1,49 +1,159 @@ -use loam_sdk::soroban_sdk::{BytesN, Lazy, String}; +use loam_sdk::{ + loam, + soroban_sdk::{ + self, contracttype, get_env, log, Address, BytesN, Env, IntoKey, Lazy, Map, String, + }, +}; use crate::{ error::Error, - version::{self, Version}, - PublishedBinary, + metadata::{ContractMetadata, PublishedContract, PublishedWasm}, + util::hash_string, + version::{self, Version, INITAL_VERSION}, }; -pub trait Riff: Lazy + Default {} +#[loam] -pub trait ABinary: Riff { +pub trait IsBinary { + /// Fetch the hash from the registry fn fetch_hash( &self, contract_name: String, version: Option, - ) -> Result, Error>; + ) -> Result, Error> { + Ok(self.fetch(contract_name, version)?.hash) + } fn current_version(&self, contract_name: String) -> Result; + /// Fetch the published binary fn fetch( &self, contract_name: String, version: Option, - ) -> Result; + ) -> Result; fn publish( &mut self, contract_name: String, + author: Address, hash: BytesN<32>, repo: Option, kind: Option, - ) -> Result; + ) -> Result<(), Error>; +} + +#[loam] +pub trait IsDeployable { + fn deploy( + &mut self, + contract_name: String, + version: Option, + deployed_name: String, + salt: Option>, + ) -> Result, Error>; +} + +#[contracttype] +#[derive(IntoKey)] +pub struct ContractRegistry(Map); + +impl Default for ContractRegistry { + fn default() -> Self { + Self(Map::new(get_env())) + } +} +impl ContractRegistry { + pub fn find_contract(&self, name: String) -> Result { + self.0 + .get(name) + .transpose() + .unwrap() + .ok_or(Error::NoSuchContract) + } + + pub fn find_version( + &self, + name: String, + version: Option, + ) -> Result { + self.find_contract(name)?.get(version) + } + + pub fn set_contract(&mut self, name: String, contract: PublishedContract) { + self.0.set(name, contract); + } } +// #[loam] + +impl IsBinary for ContractRegistry { + fn fetch( + &self, + contract_name: String, + version: Option, + ) -> Result { + self.find_version(contract_name, version) + } -pub trait Binary { - type Impl: ABinary; + fn current_version(&self, contract_name: String) -> Result { + self.find_contract(contract_name)?.most_recent_version() + } - fn fetch_hash(contract_name: String, version: Option) -> BytesN<32> { - Self::Impl::get_lazy() - .unwrap_or_default() - .fetch_hash(contract_name, version) + fn publish( + &mut self, + contract_name: String, + author: Address, + hash: BytesN<32>, + repo: Option, + kind: Option, + ) -> Result<(), Error> { + let mut contract = self + .find_contract(contract_name.clone()) + .unwrap_or_else(|_| PublishedContract::new(author)); + contract.author.require_auth(); + let keys = contract.versions.keys(); + let last_version = keys.last().transpose().unwrap().unwrap_or_default(); + log!(get_env(), "{}", last_version); + let new_version = last_version.clone().update(&kind.unwrap_or_default()); + let metadata = if let Some(repo) = repo { + ContractMetadata { repo } + } else if new_version == INITAL_VERSION { + ContractMetadata::default() + } else { + contract.get(Some(last_version))?.metadata + }; + let published_binary = PublishedWasm { + hash, + metadata, + num_deployed: 0, + }; + contract.versions.set(new_version, published_binary); + self.set_contract(contract_name, contract); + Ok(()) } +} + +impl IsDeployable for ContractRegistry { + /// Deploys a new published contract returning the deployed contract's id. + /// If no salt provided it will use the current sequence number. + fn deploy( + &mut self, + contract_name: String, + version: Option, + deployed_name: String, + salt: Option>, + ) -> Result, Error> { + let env = get_env(); + let mut binary = self.find_version(contract_name.clone(), version.clone())?; + let mut contract = self.find_contract(contract_name.clone())?; + let hash = binary.hash.clone(); + + let salt = salt.unwrap_or_else(|| hash_string(env, &deployed_name)); + binary.num_deployed += 1; + log!(env, "num_deployed {}", binary.num_deployed); + contract.set(version, binary)?; + self.set_contract(contract_name, contract); - fn fetch(contract_name: String, version: Option) -> PublishedBinary { - Self::Impl::get_lazy() - .unwrap_or_default() - .fetch(contract_name, version) + Ok(env.deployer().with_current_contract(&salt).deploy(&hash)) } } diff --git a/crates/smartdeploy/src/util.rs b/crates/smartdeploy/src/util.rs new file mode 100644 index 0000000..7db962a --- /dev/null +++ b/crates/smartdeploy/src/util.rs @@ -0,0 +1,11 @@ +use loam_sdk::soroban_sdk::{Bytes, BytesN, Env, String}; + +pub fn hash_string(env: &Env, s: &String) -> BytesN<32> { + let len = s.len() as usize; + let mut bytes = [0u8; 100]; + let bytes = &mut bytes[0..len]; + s.copy_into_slice(bytes); + let mut b = Bytes::new(env); + b.copy_from_slice(0, bytes); + env.crypto().sha256(&b) +} diff --git a/deploy.sh b/deploy.sh index 68e3602..91bf499 100755 --- a/deploy.sh +++ b/deploy.sh @@ -51,8 +51,7 @@ smartdeploy="soroban contract invoke --network futurenet --source default --id $ $smartdeploy --help if test "$FILE_HASH" = ""; then - $smartdeploy register_name --contract_name hello_world --author "$(soroban config identity address default)" - $smartdeploy publish_binary \ + $smartdeploy publish \ --contract_name hello_world \ --hash 6c453071976d247e6c8552034ba24a7b6ba95d599eb216d72a15bf8bd7176a8a \ --repo https://github.com/AhaLabs/soroban-examples/tree/0d977e1b56d3b7007855f6557248e17f37081699/hello_world diff --git a/hash.txt b/hash.txt index c83456c..ec1daed 100644 --- a/hash.txt +++ b/hash.txt @@ -1 +1 @@ -81c932f877c68531015f3d3c2875f5ca50eab46ddb4feb655fbc182ab10cf1de \ No newline at end of file +9075cf62bf352a8bdc7c839167ca35d2817af9e03fbca54f6f3845be32a1e3dd \ No newline at end of file