pink_web3/contract/
deploy.rs

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