ic_web3/contract/
deploy.rs

1//! Contract deployment utilities
2
3use crate::{
4    api::{Eth, Namespace},
5    confirm,
6    contract::{tokens::Tokenize, Contract, Options},
7    error,
8    types::{Address, Bytes, TransactionReceipt, TransactionRequest},
9    Transport,
10};
11#[cfg(feature = "signing")]
12use crate::{signing::Key, types::TransactionParameters};
13use futures::{Future, TryFutureExt};
14use std::{collections::HashMap, time};
15
16pub use crate::contract::error::deploy::Error;
17
18/// A configuration builder for contract deployment.
19#[derive(Debug)]
20pub struct Builder<T: Transport> {
21    pub(crate) eth: Eth<T>,
22    pub(crate) abi: ethabi::Contract,
23    pub(crate) options: Options,
24    pub(crate) confirmations: usize,
25    pub(crate) poll_interval: time::Duration,
26    pub(crate) linker: HashMap<String, Address>,
27}
28
29impl<T: Transport> Builder<T> {
30    /// Number of confirmations required after code deployment.
31    pub fn confirmations(mut self, confirmations: usize) -> Self {
32        self.confirmations = confirmations;
33        self
34    }
35
36    /// Deployment transaction options.
37    pub fn options(mut self, options: Options) -> Self {
38        self.options = options;
39        self
40    }
41
42    /// Confirmations poll interval.
43    pub fn poll_interval(mut self, interval: time::Duration) -> Self {
44        self.poll_interval = interval;
45        self
46    }
47
48    /// Execute deployment passing code and contructor parameters.
49    pub async fn execute<P, V>(self, code: V, params: P, from: Address) -> Result<Contract<T>, Error>
50    where
51        P: Tokenize,
52        V: AsRef<str>,
53    {
54        let transport = self.eth.transport().clone();
55        let poll_interval = self.poll_interval;
56        let confirmations = self.confirmations;
57
58        self.do_execute(code, params, from, move |tx| {
59            confirm::send_transaction_with_confirmation(transport, tx, poll_interval, confirmations)
60        })
61        .await
62    }
63    /// Execute deployment passing code and constructor parameters.
64    ///
65    /// Unlike the above `execute`, this method uses
66    /// `sign_raw_transaction_with_confirmation` instead of
67    /// `sign_transaction_with_confirmation`, which requires the account from
68    /// which the transaction is sent to be unlocked.
69    pub async fn sign_and_execute<P, V>(
70        self,
71        code: V,
72        params: P,
73        from: Address,
74        password: &str,
75    ) -> Result<Contract<T>, Error>
76    where
77        P: Tokenize,
78        V: AsRef<str>,
79    {
80        let transport = self.eth.transport().clone();
81        let poll_interval = self.poll_interval;
82        let confirmations = self.confirmations;
83
84        self.do_execute(code, params, from, move |tx| {
85            crate::api::Personal::new(transport.clone())
86                .sign_transaction(tx, password)
87                .and_then(move |signed_tx| {
88                    confirm::send_raw_transaction_with_confirmation(
89                        transport,
90                        signed_tx.raw,
91                        poll_interval,
92                        confirmations,
93                    )
94                })
95        })
96        .await
97    }
98
99    /// Execute deployment passing code and constructor parameters.
100    ///
101    /// Unlike the above `sign_and_execute`, this method allows the
102    /// caller to pass in a private key to sign the transaction with
103    /// and therefore allows deploying from an account that the
104    /// ethereum node doesn't need to know the private key for.
105    ///
106    /// An optional `chain_id` parameter can be passed to provide
107    /// replay protection for transaction signatures. Passing `None`
108    /// would create a transaction WITHOUT replay protection and
109    /// should be avoided.
110    /// You can obtain `chain_id` of the network you are connected
111    /// to using `web3.eth().chain_id()` method.
112    #[cfg(feature = "signing")]
113    pub async fn sign_with_key_and_execute<P, V, K>(
114        self,
115        code: V,
116        params: P,
117        from: K,
118        chain_id: Option<u64>,
119    ) -> Result<Contract<T>, Error>
120    where
121        P: Tokenize,
122        V: AsRef<str>,
123        K: Key,
124    {
125        let transport = self.eth.transport().clone();
126        let poll_interval = self.poll_interval;
127        let confirmations = self.confirmations;
128
129        self.do_execute(code, params, from.address(), move |tx| async move {
130            let tx = TransactionParameters {
131                nonce: tx.nonce,
132                to: tx.to,
133                gas: tx.gas.unwrap_or_else(|| 1_000_000.into()),
134                gas_price: tx.gas_price,
135                value: tx.value.unwrap_or_else(|| 0.into()),
136                data: tx
137                    .data
138                    .expect("Tried to deploy a contract but transaction data wasn't set"),
139                chain_id,
140                transaction_type: tx.transaction_type,
141                access_list: tx.access_list,
142                max_fee_per_gas: tx.max_fee_per_gas,
143                max_priority_fee_per_gas: tx.max_priority_fee_per_gas,
144            };
145            let signed_tx = crate::api::Accounts::new(transport.clone())
146                .sign_transaction(tx, from)
147                .await?;
148            confirm::send_raw_transaction_with_confirmation(
149                transport,
150                signed_tx.raw_transaction,
151                poll_interval,
152                confirmations,
153            )
154            .await
155        })
156        .await
157    }
158
159    async fn do_execute<P, V, Ft>(
160        self,
161        code: V,
162        params: P,
163        from: Address,
164        send: impl FnOnce(TransactionRequest) -> Ft,
165    ) -> Result<Contract<T>, Error>
166    where
167        P: Tokenize,
168        V: AsRef<str>,
169        Ft: Future<Output = error::Result<TransactionReceipt>>,
170    {
171        let options = self.options;
172        let eth = self.eth;
173        let abi = self.abi;
174
175        let mut code_hex = code.as_ref().to_string();
176
177        for (lib, address) in self.linker {
178            if lib.len() > 38 {
179                return Err(Error::Abi(ethabi::Error::InvalidName(
180                    "The library name should be under 39 characters.".into(),
181                )));
182            }
183            let replace = format!("__{:_<38}", lib); // This makes the required width 38 characters and will pad with `_` to match it.
184            let address: String = hex::encode(address);
185            code_hex = code_hex.replacen(&replace, &address, 1);
186        }
187        code_hex = code_hex.replace("\"", "").replace("0x", ""); // This is to fix truffle + serde_json redundant `"` and `0x`
188        let code =
189            hex::decode(&code_hex).map_err(|e| ethabi::Error::InvalidName(format!("hex decode error: {}", e)))?;
190
191        let params = params.into_tokens();
192        let data = match (abi.constructor(), params.is_empty()) {
193            (None, false) => {
194                return Err(Error::Abi(ethabi::Error::InvalidName(
195                    "Constructor is not defined in the ABI.".into(),
196                )));
197            }
198            (None, true) => code,
199            (Some(constructor), _) => constructor.encode_input(code, &params)?,
200        };
201
202        let tx = TransactionRequest {
203            from,
204            to: None,
205            gas: options.gas,
206            gas_price: options.gas_price,
207            value: options.value,
208            nonce: options.nonce,
209            data: Some(Bytes(data)),
210            condition: options.condition,
211            transaction_type: options.transaction_type,
212            access_list: options.access_list,
213            max_fee_per_gas: options.max_fee_per_gas,
214            max_priority_fee_per_gas: options.max_priority_fee_per_gas,
215        };
216        let receipt = send(tx).await?;
217        match receipt.status {
218            Some(status) if status == 0.into() => Err(Error::ContractDeploymentFailure(receipt.transaction_hash)),
219            // If the `status` field is not present we use the presence of `contract_address` to
220            // determine if deployment was successfull.
221            _ => match receipt.contract_address {
222                Some(address) => Ok(Contract::new(eth, address, abi)),
223                None => Err(Error::ContractDeploymentFailure(receipt.transaction_hash)),
224            },
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use crate::{
232        api::{self, Namespace},
233        contract::{Contract, Options},
234        rpc,
235        transports::test::TestTransport,
236        types::{Address, U256},
237    };
238    use serde_json::Value;
239    use std::collections::HashMap;
240
241    #[test]
242    fn should_deploy_a_contract() {
243        // given
244        let mut transport = TestTransport::default();
245        // Transaction Hash
246        transport.add_response(rpc::Value::String(
247            "0x70ae45a5067fdf3356aa615ca08d925a38c7ff21b486a61e79d5af3969ebc1a1".into(),
248        ));
249        // BlockFilter
250        transport.add_response(rpc::Value::String("0x0".into()));
251        // getFilterChanges
252        transport.add_response(rpc::Value::Array(vec![rpc::Value::String(
253            "0xd5311584a9867d8e129113e1ec9db342771b94bd4533aeab820a5bcc2c54878f".into(),
254        )]));
255        transport.add_response(rpc::Value::Array(vec![rpc::Value::String(
256            "0xd5311584a9867d8e129113e1ec9db342771b94bd4533aeab820a5bcc2c548790".into(),
257        )]));
258        // receipt
259        let receipt = ::serde_json::from_str::<rpc::Value>(
260            "{\"blockHash\":\"0xd5311584a9867d8e129113e1ec9db342771b94bd4533aeab820a5bcc2c54878f\",\"blockNumber\":\"0x256\",\"contractAddress\":\"0x600515dfe465f600f0c9793fa27cd2794f3ec0e1\",\"from\": \"0x407d73d8a49eeb85d32cf465507dd71d507100c1\",\"cumulativeGasUsed\":\"0xe57e0\",\"gasUsed\":\"0xe57e0\",\"logs\":[],\"logsBloom\":\"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"root\":null,\"transactionHash\":\"0x70ae45a5067fdf3356aa615ca08d925a38c7ff21b486a61e79d5af3969ebc1a1\",\"transactionIndex\":\"0x0\", \"status\": \"0x1\", \"effectiveGasPrice\": \"0x100\"}"
261        ).unwrap();
262        transport.add_response(receipt.clone());
263        // block number
264        transport.add_response(rpc::Value::String("0x25a".into()));
265        // receipt again
266        transport.add_response(receipt);
267
268        {
269            let builder = Contract::deploy(api::Eth::new(&transport), include_bytes!("./res/token.json")).unwrap();
270
271            // when
272            futures::executor::block_on(
273                builder
274                    .options(Options::with(|opt| opt.value = Some(5.into())))
275                    .confirmations(1)
276                    .execute(
277                        "0x01020304",
278                        (U256::from(1_000_000), "My Token".to_owned(), 3u64, "MT".to_owned()),
279                        Address::from_low_u64_be(5),
280                    ),
281            )
282            .unwrap()
283        };
284
285        // then
286        transport.assert_request("eth_sendTransaction", &[
287      "{\"data\":\"0x0102030400000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000084d7920546f6b656e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024d54000000000000000000000000000000000000000000000000000000000000\",\"from\":\"0x0000000000000000000000000000000000000005\",\"value\":\"0x5\"}".into(),
288    ]);
289        transport.assert_request("eth_newBlockFilter", &[]);
290        transport.assert_request("eth_getFilterChanges", &["\"0x0\"".into()]);
291        transport.assert_request("eth_getFilterChanges", &["\"0x0\"".into()]);
292        transport.assert_request(
293            "eth_getTransactionReceipt",
294            &["\"0x70ae45a5067fdf3356aa615ca08d925a38c7ff21b486a61e79d5af3969ebc1a1\"".into()],
295        );
296        transport.assert_request("eth_blockNumber", &[]);
297        transport.assert_request(
298            "eth_getTransactionReceipt",
299            &["\"0x70ae45a5067fdf3356aa615ca08d925a38c7ff21b486a61e79d5af3969ebc1a1\"".into()],
300        );
301        transport.assert_no_more_requests();
302    }
303
304    #[test]
305    fn deploy_linked_contract() {
306        use serde_json::{to_string, to_vec};
307        let mut transport = TestTransport::default();
308        let receipt = ::serde_json::from_str::<rpc::Value>(
309        "{\"blockHash\":\"0xd5311584a9867d8e129113e1ec9db342771b94bd4533aeab820a5bcc2c54878f\",\"blockNumber\":\"0x256\",\"contractAddress\":\"0x600515dfe465f600f0c9793fa27cd2794f3ec0e1\",\"from\":\"0x407d73d8a49eeb85d32cf465507dd71d507100c1\",\"cumulativeGasUsed\":\"0xe57e0\",\"gasUsed\":\"0xe57e0\",\"logs\":[],\"logsBloom\":\"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"root\":null,\"transactionHash\":\"0x70ae45a5067fdf3356aa615ca08d925a38c7ff21b486a61e79d5af3969ebc1a1\",\"transactionIndex\":\"0x0\", \"status\": \"0x1\", \"effectiveGasPrice\": \"0x100\"}"
310        ).unwrap();
311
312        for _ in 0..2 {
313            transport.add_response(rpc::Value::String(
314                "0x70ae45a5067fdf3356aa615ca08d925a38c7ff21b486a61e79d5af3969ebc1a1".into(),
315            ));
316            transport.add_response(rpc::Value::String("0x0".into()));
317            transport.add_response(rpc::Value::Array(vec![rpc::Value::String(
318                "0xd5311584a9867d8e129113e1ec9db342771b94bd4533aeab820a5bcc2c54878f".into(),
319            )]));
320            transport.add_response(rpc::Value::Array(vec![rpc::Value::String(
321                "0xd5311584a9867d8e129113e1ec9db342771b94bd4533aeab820a5bcc2c548790".into(),
322            )]));
323            transport.add_response(receipt.clone());
324            transport.add_response(rpc::Value::String("0x25a".into()));
325            transport.add_response(receipt.clone());
326        }
327
328        let lib: Value = serde_json::from_slice(include_bytes!("./res/MyLibrary.json")).unwrap();
329        let lib_abi: Vec<u8> = to_vec(&lib["abi"]).unwrap();
330        let lib_code = to_string(&lib["bytecode"]).unwrap();
331
332        let main: Value = serde_json::from_slice(include_bytes!("./res/Main.json")).unwrap();
333        let main_abi: Vec<u8> = to_vec(&main["abi"]).unwrap();
334        let main_code = to_string(&main["bytecode"]).unwrap();
335
336        let lib_address;
337        {
338            let builder = Contract::deploy(api::Eth::new(&transport), &lib_abi).unwrap();
339            lib_address = futures::executor::block_on(builder.execute(lib_code, (), Address::zero()))
340                .unwrap()
341                .address();
342        }
343
344        transport.assert_request("eth_sendTransaction", &[
345            "{\"data\":\"0x60ad61002f600b82828239805160001a6073146000811461001f57610021565bfe5b5030600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600436106056576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063f8a8fd6d14605b575b600080fd5b60616077565b6040518082815260200191505060405180910390f35b600061010090509056fea165627a7a72305820b50091adcb7ef9987dd8daa665cec572801bf8243530d70d52631f9d5ddb943e0029\",\"from\":\"0x0000000000000000000000000000000000000000\"}"
346            .into()]);
347        transport.assert_request("eth_newBlockFilter", &[]);
348        transport.assert_request("eth_getFilterChanges", &["\"0x0\"".into()]);
349        transport.assert_request("eth_getFilterChanges", &["\"0x0\"".into()]);
350        transport.assert_request(
351            "eth_getTransactionReceipt",
352            &["\"0x70ae45a5067fdf3356aa615ca08d925a38c7ff21b486a61e79d5af3969ebc1a1\"".into()],
353        );
354        transport.assert_request("eth_blockNumber", &[]);
355        transport.assert_request(
356            "eth_getTransactionReceipt",
357            &["\"0x70ae45a5067fdf3356aa615ca08d925a38c7ff21b486a61e79d5af3969ebc1a1\"".into()],
358        );
359        transport.assert_no_more_requests();
360        {
361            let builder = Contract::deploy_from_truffle(api::Eth::new(&transport), &main_abi, {
362                let mut linker = HashMap::new();
363                linker.insert("MyLibrary", lib_address);
364                linker
365            })
366            .unwrap();
367            let _ = futures::executor::block_on(builder.execute(main_code, (), Address::zero())).unwrap();
368        }
369
370        transport.assert_request("eth_sendTransaction", &[
371            "{\"data\":\"0x608060405234801561001057600080fd5b5061013f806100206000396000f3fe608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063f8a8fd6d14610046575b600080fd5b34801561005257600080fd5b5061005b610071565b6040518082815260200191505060405180910390f35b600073600515dfe465f600f0c9793fa27cd2794f3ec0e163f8a8fd6d6040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160206040518083038186803b1580156100d357600080fd5b505af41580156100e7573d6000803e3d6000fd5b505050506040513d60208110156100fd57600080fd5b810190808051906020019092919050505090509056fea165627a7a72305820580d3776b3d132142f431e141a2e20bd4dd4907fa304feea7b604e8f39ed59520029\",\"from\":\"0x0000000000000000000000000000000000000000\"}"
372            .into()]);
373
374        transport.assert_request("eth_newBlockFilter", &[]);
375        transport.assert_request("eth_getFilterChanges", &["\"0x0\"".into()]);
376        transport.assert_request("eth_getFilterChanges", &["\"0x0\"".into()]);
377        transport.assert_request(
378            "eth_getTransactionReceipt",
379            &["\"0x70ae45a5067fdf3356aa615ca08d925a38c7ff21b486a61e79d5af3969ebc1a1\"".into()],
380        );
381        transport.assert_request("eth_blockNumber", &[]);
382        transport.assert_request(
383            "eth_getTransactionReceipt",
384            &["\"0x70ae45a5067fdf3356aa615ca08d925a38c7ff21b486a61e79d5af3969ebc1a1\"".into()],
385        );
386        transport.assert_no_more_requests();
387    }
388}