phink_lib/contract/
remote.rs

1use anyhow::{
2    bail,
3    Context,
4};
5use frame_support::{
6    __private::BasicExternalities,
7    pallet_prelude::Weight,
8    traits::fungible::Inspect,
9};
10use migration::v13;
11use pallet_contracts::{
12    migration,
13    Code,
14    CollectEvents,
15    Config,
16    ContractResult,
17    DebugInfo,
18    Determinism,
19    ExecReturnValue,
20};
21use sp_core::{
22    crypto::AccountId32,
23    storage::Storage,
24    H256,
25};
26use sp_runtime::{
27    DispatchError,
28    ModuleError,
29};
30use std::{
31    fmt::{
32        Display,
33        Formatter,
34    },
35    fs,
36    path::PathBuf,
37};
38use v13::ContractInfoOf;
39
40use payload::PayloadCrafter;
41
42use crate::{
43    cli::{
44        config::Configuration,
45        ziggy::ZiggyConfig,
46    },
47    contract::{
48        custom::preferences::{
49            DevelopperPreferences,
50            Preferences,
51        },
52        payload,
53        runtime::{
54            AccountId,
55            Contracts,
56            Runtime,
57        },
58    },
59    cover::trace::CoverageTrace,
60    instrumenter::instrumentation::Instrumenter,
61    ResultOf,
62};
63
64pub type BalanceOf<T> =
65    <<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
66
67pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
68pub type EventRecord = frame_system::EventRecord<
69    <Runtime as frame_system::Config>::RuntimeEvent,
70    <Runtime as frame_system::Config>::Hash,
71>;
72pub type ContractResponse =
73    ContractResult<Result<ExecReturnValue, DispatchError>, u128, EventRecord>;
74
75#[derive(Clone, scale_info::TypeInfo)]
76pub struct FullContractResponse(pub ContractResponse);
77
78impl Display for FullContractResponse {
79    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80        write!(
81            f,
82            "{}",
83            self.clone()
84                .debug_message()
85                .remove_cov_from_trace()
86                .replace("\n", " ")
87        )
88    }
89}
90impl FullContractResponse {
91    pub fn get_response(&self) -> &ContractResponse {
92        &self.0
93    }
94
95    pub fn from_contract_result(
96        c: ContractResult<Result<ExecReturnValue, DispatchError>, u128, EventRecord>,
97    ) -> Self {
98        Self(c)
99    }
100    pub fn result(&self) -> &Result<ExecReturnValue, DispatchError> {
101        &self.0.result
102    }
103
104    pub fn failed(&self) -> bool {
105        self.0.result.is_err()
106    }
107
108    pub fn get(&self) -> &ContractResponse {
109        &self.0
110    }
111
112    pub fn debug_message(self) -> CoverageTrace {
113        CoverageTrace::from(self.0.debug_message)
114    }
115    pub fn is_trapped(&self) -> bool {
116        if let Err(DispatchError::Module(ModuleError { message, .. })) = &self.0.result {
117            if *message == Some("ContractTrapped") {
118                return true;
119            }
120        }
121        false
122    }
123}
124
125#[derive(Clone)]
126pub struct ContractSetup {
127    pub genesis: Storage,
128    pub contract_address: AccountIdOf<Runtime>,
129    pub json_specs: String,
130    pub path_to_specs: PathBuf,
131}
132
133impl ContractSetup {
134    pub const DEFAULT_GAS_LIMIT: Weight = Weight::from_parts(100_000_000_000, 3 * 1024 * 1024);
135    pub const DEFAULT_DEPLOYER: AccountId32 = AccountId32::new([1u8; 32]);
136
137    /// Create a proper genesis storage, deploy and instantiate a given ink!
138    /// contract
139    pub fn initialize_wasm(config: ZiggyConfig) -> ResultOf<Self> {
140        let finder = Instrumenter::new(config.clone())
141            .find()
142            .context("Couldn't execute `find` for this current config")?;
143        let wasm_bytes = fs::read(&finder.wasm_path)
144            .context(format!("Couldn't read WASM from {:?}", finder.wasm_path))?;
145
146        let conf = config.config();
147        let contract_address: &AccountIdOf<Runtime> = conf.deployer_address();
148        if conf.verbose {
149            println!("🛠️Initializing contract address from the origin: {contract_address:?}");
150        }
151
152        let path_to_specs = finder.specs_path;
153        let json_specs = fs::read_to_string(&path_to_specs).context(format!(
154            "Couldn't read JSON from {path_to_specs:?}. \
155            Check that you have a WASM AND JSON file in your target/ directory"
156        ))?;
157
158        let genesis: (Storage, AccountIdOf<Runtime>) = {
159            let storage = <Preferences as DevelopperPreferences>::runtime_storage();
160
161            let mut chain = BasicExternalities::new(storage.clone());
162            let address = chain.execute_with(|| {
163                let _ = <Preferences as DevelopperPreferences>::on_contract_initialize(); //This is optional and can `Err()` easily, so we use `let _`
164
165                if conf.verbose {
166                    println!("📤 Starting upload of WASM bytes by: {contract_address:?}");
167                }
168
169                let code_hash = Self::upload(&wasm_bytes, contract_address)?;
170
171                let new_contract_address: AccountIdOf<Runtime> = Self::instantiate(&json_specs, code_hash, contract_address, conf)
172                    .context("Can't fetch the contract address because of incorrect instantiation")?;
173
174                // We verify if the contract is correctly instantiated
175                if !ContractInfoOf::<Runtime>::contains_key(&new_contract_address) {
176                    bail!(
177                        "Contract instantiation failed!
178                            This error is likely due to a misconfigured constructor payload in the configuration file.
179                            Please ensure the correct payload for the constructor (selector + parameters) is provided, just as you would for a regular deployment. You can use the `constructor_payload` field inside the TOML configuration file for this purpose.
180                            To generate your payload, please use `cargo contract`, for instance
181                            ```sh
182                            cargo contract encode --message \"new\" --args 1234 1337 \"0x8bb565d32618e40e8b9991c00d05b52a89ddbed0c7d9103be5610ab8a713fc67\" \"0x2a18c7d454ba9cc46f97fff2f048db136d975fb1401e75c09ed03050864bcd19\" \"0xbf0108f5882aee2e97f84f054c1645c1598499e9dfcf179e367e4d41c3130ee8\" -- target/ink/multi_contract_caller.\
183                            \
184                            Encoded data: 9BAE9D5E...3130EE8\
185                            ```"
186                    );
187                }
188                Ok(new_contract_address)
189            })?;
190
191            (chain.into_storages(), address)
192        };
193
194        Ok(Self {
195            genesis: genesis.0,
196            contract_address: genesis.1,
197            json_specs,
198            path_to_specs,
199        })
200    }
201
202    /// Execute a function `payload` from the instantiated contract
203    pub fn call(
204        self,
205        payload: &[u8],
206        who: u8,
207        transfer_value: BalanceOf<Runtime>,
208        config: Configuration,
209    ) -> FullContractResponse {
210        FullContractResponse::from_contract_result(Contracts::bare_call(
211            AccountId32::new([who; 32]),
212            self.contract_address,
213            transfer_value,
214            config.default_gas_limit.unwrap_or(Self::DEFAULT_GAS_LIMIT),
215            Configuration::parse_balance(&config.storage_deposit_limit),
216            payload.to_owned(),
217            DebugInfo::UnsafeDebug,
218            CollectEvents::UnsafeCollect,
219            Determinism::Enforced,
220        ))
221    }
222
223    pub fn upload(wasm_bytes: &[u8], who: &AccountId) -> ResultOf<H256> {
224        let upload_result = Contracts::bare_upload_code(
225            who.clone(),
226            Vec::from(wasm_bytes),
227            None,
228            Determinism::Enforced,
229        );
230        match upload_result {
231            Ok(upload_info) => {
232                let hash = upload_info.code_hash;
233                println!("✅ Upload successful. Code hash: {hash}",);
234                Ok(hash)
235            }
236            Err(e) => {
237                bail!("❌ Upload failed for: {who:?} with error: {e:?}");
238            }
239        }
240    }
241
242    pub fn instantiate(
243        json_specs: &str,
244        code_hash: H256,
245        who: &AccountId,
246        config: &Configuration,
247    ) -> ResultOf<AccountIdOf<Runtime>> {
248        let data: Vec<u8> = if let Some(payload) = &config.constructor_payload {
249            hex::decode(payload.replace(" ", ""))
250                .context("Impossible to hex-decode this. Check your config file")?
251        } else {
252            PayloadCrafter::extract_constructor(json_specs)
253                .context("Couldn't extract the constructor from the JSON specs")?
254                .into()
255        };
256
257        let initial_value = Configuration::parse_balance(&config.instantiate_initial_value);
258
259        let instantiate = Contracts::bare_instantiate(
260            who.clone(),
261            initial_value.unwrap_or(0),
262            config.default_gas_limit.unwrap_or_default(),
263            None,
264            Code::Existing(code_hash),
265            data,
266            vec![],
267            DebugInfo::UnsafeDebug,
268            CollectEvents::UnsafeCollect,
269        );
270
271        match instantiate.result {
272            Ok(contract_info) => {
273                println!("🔍 Instantiated the contract, contract's account is {who:?}");
274                Ok(contract_info.account_id)
275            }
276            Err(e) => {
277                let debug = String::from_utf8_lossy(instantiate.debug_message.as_ref());
278                bail!("❌ Failed to instantiate the contract, double check your `constructor_payload` please ({e:?}). Details : {debug}");
279            }
280        }
281    }
282}