use std::{
fmt::Debug,
mem,
sync::{Arc, Mutex},
};
pub use contract_transcode;
use contract_transcode::ContractMessageTranscoder;
use error::SessionError;
use frame_support::{traits::fungible::Inspect, weights::Weight};
use ink_sandbox::{
api::prelude::*, AccountIdFor, ContractExecResultFor, ContractInstantiateResultFor, Sandbox,
};
use parity_scale_codec::Decode;
pub use record::{EventBatch, Record};
use crate::{
minimal::MinimalSandboxRuntime,
pallet_contracts::{Config, Determinism},
pallet_contracts_debugging::{InterceptingExt, TracingExt},
session::mock::MockRegistry,
};
pub mod mock;
use mock::MockingExtension;
pub mod bundle;
pub mod error;
pub mod mocking_api;
mod record;
mod transcoding;
pub use bundle::ContractBundle;
use self::mocking_api::MockingApi;
use crate::{
errors::MessageResult,
session::transcoding::TranscoderRegistry,
};
type BalanceOf<R> = <<R as Config>::Currency as Inspect<AccountIdFor<R>>>::Balance;
type HashFor<R> = <R as frame_system::Config>::Hash;
pub const NO_ARGS: &[String] = &[];
pub const NO_SALT: Vec<u8> = vec![];
pub const NO_ENDOWMENT: Option<BalanceOf<MinimalSandboxRuntime>> = None;
pub struct Session<T: Sandbox>
where
T::Runtime: Config,
{
sandbox: T,
actor: AccountIdFor<T::Runtime>,
gas_limit: Weight,
determinism: Determinism,
transcoders: TranscoderRegistry<AccountIdFor<T::Runtime>>,
record: Record<T::Runtime>,
mocks: Arc<Mutex<MockRegistry<AccountIdFor<T::Runtime>>>>,
}
impl<T: Sandbox> Default for Session<T>
where
T::Runtime: Config,
T: Default,
{
fn default() -> Self {
let mocks = Arc::new(Mutex::new(MockRegistry::new()));
let mut sandbox = T::default();
sandbox.register_extension(InterceptingExt(Box::new(MockingExtension {
mock_registry: Arc::clone(&mocks),
})));
Self {
sandbox,
mocks,
actor: T::default_actor(),
gas_limit: T::default_gas_limit(),
determinism: Determinism::Enforced,
transcoders: TranscoderRegistry::new(),
record: Default::default(),
}
}
}
impl<T: Sandbox> Session<T>
where
T::Runtime: Config,
{
pub fn with_actor(self, actor: AccountIdFor<T::Runtime>) -> Self {
Self { actor, ..self }
}
pub fn get_actor(&self) -> AccountIdFor<T::Runtime> {
self.actor.clone()
}
pub fn set_actor(&mut self, actor: AccountIdFor<T::Runtime>) -> AccountIdFor<T::Runtime> {
mem::replace(&mut self.actor, actor)
}
pub fn with_gas_limit(self, gas_limit: Weight) -> Self {
Self { gas_limit, ..self }
}
pub fn set_gas_limit(&mut self, gas_limit: Weight) -> Weight {
mem::replace(&mut self.gas_limit, gas_limit)
}
pub fn get_gas_limit(&self) -> Weight {
self.gas_limit
}
pub fn with_determinism(self, determinism: Determinism) -> Self {
Self {
determinism,
..self
}
}
pub fn set_determinism(&mut self, determinism: Determinism) -> Determinism {
mem::replace(&mut self.determinism, determinism)
}
pub fn with_transcoder(
mut self,
contract_address: AccountIdFor<T::Runtime>,
transcoder: &Arc<ContractMessageTranscoder>,
) -> Self {
self.set_transcoder(contract_address, transcoder);
self
}
pub fn set_transcoder(
&mut self,
contract_address: AccountIdFor<T::Runtime>,
transcoder: &Arc<ContractMessageTranscoder>,
) {
self.transcoders.register(contract_address, transcoder);
}
pub fn sandbox(&mut self) -> &mut T {
&mut self.sandbox
}
pub fn record(&self) -> &Record<T::Runtime> {
&self.record
}
pub fn mocking_api(&mut self) -> &mut impl MockingApi<T::Runtime> {
self
}
pub fn deploy_and<S: AsRef<str> + Debug>(
mut self,
contract_bytes: Vec<u8>,
constructor: &str,
args: &[S],
salt: Vec<u8>,
endowment: Option<BalanceOf<T::Runtime>>,
transcoder: &Arc<ContractMessageTranscoder>,
) -> Result<Self, SessionError> {
self.deploy(
contract_bytes,
constructor,
args,
salt,
endowment,
transcoder,
)
.map(|_| self)
}
fn record_events<V>(&mut self, recording: impl FnOnce(&mut Self) -> V) -> V {
let start = self.sandbox.events().len();
let result = recording(self);
let events = self.sandbox.events()[start..].to_vec();
self.record.push_event_batches(events);
result
}
pub fn deploy<S: AsRef<str> + Debug>(
&mut self,
contract_bytes: Vec<u8>,
constructor: &str,
args: &[S],
salt: Vec<u8>,
endowment: Option<BalanceOf<T::Runtime>>,
transcoder: &Arc<ContractMessageTranscoder>,
) -> Result<AccountIdFor<T::Runtime>, SessionError> {
let data = transcoder
.encode(constructor, args)
.map_err(|err| SessionError::Encoding(err.to_string()))?;
let result = self.record_events(|session| {
session.sandbox.deploy_contract(
contract_bytes,
endowment.unwrap_or_default(),
data,
salt,
session.actor.clone(),
session.gas_limit,
None,
)
});
let ret = match &result.result {
Ok(exec_result) if exec_result.result.did_revert() => {
Err(SessionError::DeploymentReverted)
}
Ok(exec_result) => {
let address = exec_result.account_id.clone();
self.record.push_deploy_return(address.clone());
self.transcoders.register(address.clone(), transcoder);
Ok(address)
}
Err(err) => Err(SessionError::DeploymentFailed(*err)),
};
self.record.push_deploy_result(result);
ret
}
pub fn deploy_bundle<S: AsRef<str> + Debug>(
&mut self,
contract_file: ContractBundle,
constructor: &str,
args: &[S],
salt: Vec<u8>,
endowment: Option<BalanceOf<T::Runtime>>,
) -> Result<AccountIdFor<T::Runtime>, SessionError> {
self.deploy(
contract_file.wasm,
constructor,
args,
salt,
endowment,
&contract_file.transcoder,
)
}
pub fn dry_run_deployment<S: AsRef<str> + Debug>(
&mut self,
contract_file: ContractBundle,
constructor: &str,
args: &[S],
salt: Vec<u8>,
endowment: Option<BalanceOf<T::Runtime>>,
) -> Result<ContractInstantiateResultFor<T::Runtime>, SessionError> {
let data = contract_file
.transcoder
.encode(constructor, args)
.map_err(|err| SessionError::Encoding(err.to_string()))?;
Ok(self.sandbox.dry_run(|sandbox| {
sandbox.deploy_contract(
contract_file.wasm,
endowment.unwrap_or_default(),
data,
salt,
self.actor.clone(),
self.gas_limit,
None,
)
}))
}
pub fn deploy_bundle_and<S: AsRef<str> + Debug>(
mut self,
contract_file: ContractBundle,
constructor: &str,
args: &[S],
salt: Vec<u8>,
endowment: Option<BalanceOf<T::Runtime>>,
) -> Result<Self, SessionError> {
self.deploy_bundle(contract_file, constructor, args, salt, endowment)
.map(|_| self)
}
pub fn upload_and(mut self, contract_bytes: Vec<u8>) -> Result<Self, SessionError> {
self.upload(contract_bytes).map(|_| self)
}
pub fn upload(&mut self, contract_bytes: Vec<u8>) -> Result<HashFor<T::Runtime>, SessionError> {
let result = self.sandbox.upload_contract(
contract_bytes,
self.actor.clone(),
None,
self.determinism,
);
result
.map(|upload_result| upload_result.code_hash)
.map_err(SessionError::UploadFailed)
}
pub fn upload_bundle_and(self, contract_file: ContractBundle) -> Result<Self, SessionError> {
self.upload_and(contract_file.wasm)
}
pub fn upload_bundle(
&mut self,
contract_file: ContractBundle,
) -> Result<HashFor<T::Runtime>, SessionError> {
self.upload(contract_file.wasm)
}
pub fn call_and<S: AsRef<str> + Debug>(
mut self,
message: &str,
args: &[S],
endowment: Option<BalanceOf<T::Runtime>>,
) -> Result<Self, SessionError> {
self.call_internal::<_, ()>(None, message, args, endowment)
.map(|_| self)
}
pub fn call_with_address_and<S: AsRef<str> + Debug>(
mut self,
address: AccountIdFor<T::Runtime>,
message: &str,
args: &[S],
endowment: Option<BalanceOf<T::Runtime>>,
) -> Result<Self, SessionError> {
self.call_internal::<_, ()>(Some(address), message, args, endowment)
.map(|_| self)
}
pub fn call<S: AsRef<str> + Debug, V: Decode>(
&mut self,
message: &str,
args: &[S],
endowment: Option<BalanceOf<T::Runtime>>,
) -> Result<MessageResult<V>, SessionError> {
self.call_internal::<_, V>(None, message, args, endowment)
}
pub fn call_and_expect_error<S: AsRef<str> + Debug, E: Debug + Decode>(
&mut self,
message: &str,
args: &[S],
endowment: Option<BalanceOf<T::Runtime>>,
) -> Result<E, SessionError> {
Ok(self
.call_internal::<_, Result<(), E>>(None, message, args, endowment)
.expect_err("Call should fail")
.decode_revert::<Result<(), E>>()?
.expect("Call should return an error")
.expect_err("Call should return an error"))
}
pub fn call_with_address<S: AsRef<str> + Debug, V: Decode>(
&mut self,
address: AccountIdFor<T::Runtime>,
message: &str,
args: &[S],
endowment: Option<BalanceOf<T::Runtime>>,
) -> Result<MessageResult<V>, SessionError> {
self.call_internal(Some(address), message, args, endowment)
}
pub fn dry_run_call<S: AsRef<str> + Debug>(
&mut self,
address: AccountIdFor<T::Runtime>,
message: &str,
args: &[S],
endowment: Option<BalanceOf<T::Runtime>>,
) -> Result<ContractExecResultFor<T::Runtime>, SessionError> {
let data = self
.transcoders
.get(&address)
.as_ref()
.ok_or(SessionError::NoTranscoder)?
.encode(message, args)
.map_err(|err| SessionError::Encoding(err.to_string()))?;
Ok(self.sandbox.dry_run(|sandbox| {
sandbox.call_contract(
address,
endowment.unwrap_or_default(),
data,
self.actor.clone(),
self.gas_limit,
None,
self.determinism,
)
}))
}
fn call_internal<S: AsRef<str> + Debug, V: Decode>(
&mut self,
address: Option<AccountIdFor<T::Runtime>>,
message: &str,
args: &[S],
endowment: Option<BalanceOf<T::Runtime>>,
) -> Result<MessageResult<V>, SessionError> {
let address = match address {
Some(address) => address,
None => self
.record
.deploy_returns()
.last()
.ok_or(SessionError::NoContract)?
.clone(),
};
let data = self
.transcoders
.get(&address)
.as_ref()
.ok_or(SessionError::NoTranscoder)?
.encode(message, args)
.map_err(|err| SessionError::Encoding(err.to_string()))?;
let result = self.record_events(|session| {
session.sandbox.call_contract(
address,
endowment.unwrap_or_default(),
data,
session.actor.clone(),
session.gas_limit,
None,
session.determinism,
)
});
let ret = match &result.result {
Ok(exec_result) if exec_result.did_revert() => {
Err(SessionError::CallReverted(exec_result.data.clone()))
}
Ok(exec_result) => {
self.record.push_call_return(exec_result.data.clone());
self.record.last_call_return_decoded::<V>()
}
Err(err) => Err(SessionError::CallFailed(*err)),
};
self.record.push_call_result(result);
ret
}
pub fn set_tracing_extension(&mut self, d: TracingExt) {
self.sandbox.register_extension(d);
}
}