cs_mwc_web3/api/
accounts.rs

1//! Partial implementation of the `Accounts` namespace.
2
3use crate::{api::Namespace, signing, types::H256, Transport};
4
5/// `Accounts` namespace
6#[derive(Debug, Clone)]
7pub struct Accounts<T> {
8    transport: T,
9}
10
11impl<T: Transport> Namespace<T> for Accounts<T> {
12    fn new(transport: T) -> Self
13    where
14        Self: Sized,
15    {
16        Accounts { transport }
17    }
18
19    fn transport(&self) -> &T {
20        &self.transport
21    }
22}
23
24impl<T: Transport> Accounts<T> {
25    /// Hash a message according to EIP-191.
26    ///
27    /// The data is a UTF-8 encoded string and will enveloped as follows:
28    /// `"\x19Ethereum Signed Message:\n" + message.length + message` and hashed
29    /// using keccak256.
30    pub fn hash_message<S>(&self, message: S) -> H256
31    where
32        S: AsRef<[u8]>,
33    {
34        let message = message.as_ref();
35
36        let mut eth_message = format!("\x19Ethereum Signed Message:\n{}", message.len()).into_bytes();
37        eth_message.extend_from_slice(message);
38
39        signing::keccak256(&eth_message).into()
40    }
41}
42
43#[cfg(feature = "signing")]
44mod accounts_signing {
45    use super::*;
46    use crate::{
47        api::Web3,
48        error,
49        signing::Signature,
50        types::{
51            Address, Bytes, Recovery, RecoveryMessage, SignedData, SignedTransaction, TransactionParameters, U256,
52        },
53    };
54    use rlp::RlpStream;
55    use std::convert::TryInto;
56
57    impl<T: Transport> Accounts<T> {
58        /// Gets the parent `web3` namespace
59        fn web3(&self) -> Web3<T> {
60            Web3::new(self.transport.clone())
61        }
62
63        /// Signs an Ethereum transaction with a given private key.
64        ///
65        /// Transaction signing can perform RPC requests in order to fill missing
66        /// parameters required for signing `nonce`, `gas_price` and `chain_id`. Note
67        /// that if all transaction parameters were provided, this future will resolve
68        /// immediately.
69        pub async fn sign_transaction<K: signing::Key>(
70            &self,
71            tx: TransactionParameters,
72            key: K,
73        ) -> error::Result<SignedTransaction> {
74            macro_rules! maybe {
75                ($o: expr, $f: expr) => {
76                    async {
77                        match $o {
78                            Some(value) => Ok(value),
79                            None => $f.await,
80                        }
81                    }
82                };
83            }
84            let from = key.address();
85            let (nonce, gas_price, chain_id) = futures::future::try_join3(
86                maybe!(tx.nonce, self.web3().eth().transaction_count(from, None)),
87                maybe!(tx.gas_price, self.web3().eth().gas_price()),
88                maybe!(tx.chain_id.map(U256::from), self.web3().eth().chain_id()),
89            )
90            .await?;
91            let chain_id = chain_id.as_u64();
92            let tx = Transaction {
93                to: tx.to,
94                nonce,
95                gas: tx.gas,
96                gas_price,
97                value: tx.value,
98                data: tx.data.0,
99            };
100            let signed = tx.sign(key, chain_id);
101            Ok(signed)
102        }
103
104        /// Sign arbitrary string data.
105        ///
106        /// The data is UTF-8 encoded and enveloped the same way as with
107        /// `hash_message`. The returned signed data's signature is in 'Electrum'
108        /// notation, that is the recovery value `v` is either `27` or `28` (as
109        /// opposed to the standard notation where `v` is either `0` or `1`). This
110        /// is important to consider when using this signature with other crates.
111        pub fn sign<S>(&self, message: S, key: impl signing::Key) -> SignedData
112        where
113            S: AsRef<[u8]>,
114        {
115            let message = message.as_ref();
116            let message_hash = self.hash_message(message);
117
118            let signature = key
119                .sign(&message_hash.as_bytes(), None)
120                .expect("hash is non-zero 32-bytes; qed");
121            let v = signature
122                .v
123                .try_into()
124                .expect("signature recovery in electrum notation always fits in a u8");
125
126            let signature_bytes = Bytes({
127                let mut bytes = Vec::with_capacity(65);
128                bytes.extend_from_slice(signature.r.as_bytes());
129                bytes.extend_from_slice(signature.s.as_bytes());
130                bytes.push(v);
131                bytes
132            });
133
134            // We perform this allocation only after all previous fallible actions have completed successfully.
135            let message = message.to_owned();
136
137            SignedData {
138                message,
139                message_hash,
140                v,
141                r: signature.r,
142                s: signature.s,
143                signature: signature_bytes,
144            }
145        }
146
147        /// Recovers the Ethereum address which was used to sign the given data.
148        ///
149        /// Recovery signature data uses 'Electrum' notation, this means the `v`
150        /// value is expected to be either `27` or `28`.
151        pub fn recover<R>(&self, recovery: R) -> error::Result<Address>
152        where
153            R: Into<Recovery>,
154        {
155            let recovery = recovery.into();
156            let message_hash = match recovery.message {
157                RecoveryMessage::Data(ref message) => self.hash_message(message),
158                RecoveryMessage::Hash(hash) => hash,
159            };
160            let (signature, recovery_id) = recovery
161                .as_signature()
162                .ok_or_else(|| error::Error::Recovery(signing::RecoveryError::InvalidSignature))?;
163            let address = signing::recover(message_hash.as_bytes(), &signature, recovery_id)?;
164            Ok(address)
165        }
166    }
167    /// A transaction used for RLP encoding, hashing and signing.
168    pub struct Transaction {
169        pub to: Option<Address>,
170        pub nonce: U256,
171        pub gas: U256,
172        pub gas_price: U256,
173        pub value: U256,
174        pub data: Vec<u8>,
175    }
176
177    impl Transaction {
178        /// RLP encode an unsigned transaction for the specified chain ID.
179        fn rlp_append_unsigned(&self, rlp: &mut RlpStream, chain_id: u64) {
180            rlp.begin_list(9);
181            rlp.append(&self.nonce);
182            rlp.append(&self.gas_price);
183            rlp.append(&self.gas);
184            if let Some(to) = self.to {
185                rlp.append(&to);
186            } else {
187                rlp.append(&"");
188            }
189            rlp.append(&self.value);
190            rlp.append(&self.data);
191            rlp.append(&chain_id);
192            rlp.append(&0u8);
193            rlp.append(&0u8);
194        }
195
196        /// RLP encode a signed transaction with the specified signature.
197        fn rlp_append_signed(&self, rlp: &mut RlpStream, signature: &Signature) {
198            rlp.begin_list(9);
199            rlp.append(&self.nonce);
200            rlp.append(&self.gas_price);
201            rlp.append(&self.gas);
202            if let Some(to) = self.to {
203                rlp.append(&to);
204            } else {
205                rlp.append(&"");
206            }
207            rlp.append(&self.value);
208            rlp.append(&self.data);
209            rlp.append(&signature.v);
210            rlp.append(&U256::from_big_endian(signature.r.as_bytes()));
211            rlp.append(&U256::from_big_endian(signature.s.as_bytes()));
212        }
213
214        /// Sign and return a raw signed transaction.
215        pub fn sign(self, sign: impl signing::Key, chain_id: u64) -> SignedTransaction {
216            let mut rlp = RlpStream::new();
217            self.rlp_append_unsigned(&mut rlp, chain_id);
218
219            let hash = signing::keccak256(rlp.as_raw());
220            let signature = sign
221                .sign(&hash, Some(chain_id))
222                .expect("hash is non-zero 32-bytes; qed");
223
224            rlp.clear();
225            self.rlp_append_signed(&mut rlp, &signature);
226
227            let transaction_hash = signing::keccak256(rlp.as_raw()).into();
228            let raw_transaction = rlp.out().to_vec().into();
229
230            SignedTransaction {
231                message_hash: hash.into(),
232                v: signature.v,
233                r: signature.r,
234                s: signature.s,
235                raw_transaction,
236                transaction_hash,
237            }
238        }
239    }
240}
241
242#[cfg(all(test, not(target_arch = "wasm32")))]
243mod tests {
244    use super::*;
245    use crate::{
246        signing::{SecretKey, SecretKeyRef},
247        transports::test::TestTransport,
248        types::{Address, Recovery, SignedTransaction, TransactionParameters, U256},
249    };
250    use accounts_signing::*;
251    use hex_literal::hex;
252    use serde_json::json;
253
254    #[test]
255    fn accounts_sign_transaction() {
256        // retrieved test vector from:
257        // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
258
259        let tx = TransactionParameters {
260            to: Some(hex!("F0109fC8DF283027b6285cc889F5aA624EaC1F55").into()),
261            value: 1_000_000_000.into(),
262            gas: 2_000_000.into(),
263            ..Default::default()
264        };
265        let key = SecretKey::from_slice(&hex!(
266            "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
267        ))
268        .unwrap();
269        let nonce = U256::zero();
270        let gas_price = U256::from(21_000_000_000u128);
271        let chain_id = "0x1";
272        let from: Address = signing::secret_key_address(&key);
273
274        let mut transport = TestTransport::default();
275        transport.add_response(json!(nonce));
276        transport.add_response(json!(gas_price));
277        transport.add_response(json!(chain_id));
278
279        let signed = {
280            let accounts = Accounts::new(&transport);
281            futures::executor::block_on(accounts.sign_transaction(tx, &key))
282        };
283
284        transport.assert_request(
285            "eth_getTransactionCount",
286            &[json!(from).to_string(), json!("latest").to_string()],
287        );
288        transport.assert_request("eth_gasPrice", &[]);
289        transport.assert_request("eth_chainId", &[]);
290        transport.assert_no_more_requests();
291
292        let expected = SignedTransaction {
293            message_hash: hex!("88cfbd7e51c7a40540b233cf68b62ad1df3e92462f1c6018d6d67eae0f3b08f5").into(),
294            v: 0x25,
295            r: hex!("c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895").into(),
296            s: hex!("727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68").into(),
297            raw_transaction: hex!("f869808504e3b29200831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a0c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895a0727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68").into(),
298            transaction_hash: hex!("de8db924885b0803d2edc335f745b2b8750c8848744905684c20b987443a9593").into(),
299        };
300
301        assert_eq!(signed, Ok(expected));
302    }
303
304    #[test]
305    fn accounts_sign_transaction_with_all_parameters() {
306        let key = SecretKey::from_slice(&hex!(
307            "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
308        ))
309        .unwrap();
310
311        let accounts = Accounts::new(TestTransport::default());
312        futures::executor::block_on(accounts.sign_transaction(
313            TransactionParameters {
314                nonce: Some(0.into()),
315                gas_price: Some(1.into()),
316                chain_id: Some(42),
317                ..Default::default()
318            },
319            &key,
320        ))
321        .unwrap();
322
323        // sign_transaction makes no requests when all parameters are specified
324        accounts.transport().assert_no_more_requests();
325    }
326
327    #[test]
328    fn accounts_hash_message() {
329        // test vector taken from:
330        // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#hashmessage
331
332        let accounts = Accounts::new(TestTransport::default());
333        let hash = accounts.hash_message("Hello World");
334
335        assert_eq!(
336            hash,
337            hex!("a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2").into()
338        );
339
340        // this method does not actually make any requests.
341        accounts.transport().assert_no_more_requests();
342    }
343
344    #[test]
345    fn accounts_sign() {
346        // test vector taken from:
347        // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign
348
349        let accounts = Accounts::new(TestTransport::default());
350
351        let key = SecretKey::from_slice(&hex!(
352            "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
353        ))
354        .unwrap();
355        let signed = accounts.sign("Some data", SecretKeyRef::new(&key));
356
357        assert_eq!(
358            signed.message_hash,
359            hex!("1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655").into()
360        );
361        assert_eq!(
362            signed.signature.0,
363            hex!("b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c")
364        );
365
366        // this method does not actually make any requests.
367        accounts.transport().assert_no_more_requests();
368    }
369
370    #[test]
371    fn accounts_recover() {
372        // test vector taken from:
373        // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#recover
374
375        let accounts = Accounts::new(TestTransport::default());
376
377        let v = 0x1cu64;
378        let r = hex!("b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd").into();
379        let s = hex!("6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029").into();
380
381        let recovery = Recovery::new("Some data", v, r, s);
382        assert_eq!(
383            accounts.recover(recovery).unwrap(),
384            hex!("2c7536E3605D9C16a7a3D7b1898e529396a65c23").into()
385        );
386
387        // this method does not actually make any requests.
388        accounts.transport().assert_no_more_requests();
389    }
390
391    #[test]
392    fn accounts_recover_signed() {
393        let key = SecretKey::from_slice(&hex!(
394            "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
395        ))
396        .unwrap();
397        let address: Address = signing::secret_key_address(&key);
398
399        let accounts = Accounts::new(TestTransport::default());
400
401        let signed = accounts.sign("rust-web3 rocks!", &key);
402        let recovered = accounts.recover(&signed).unwrap();
403        assert_eq!(recovered, address);
404
405        let signed = futures::executor::block_on(accounts.sign_transaction(
406            TransactionParameters {
407                nonce: Some(0.into()),
408                gas_price: Some(1.into()),
409                chain_id: Some(42),
410                ..Default::default()
411            },
412            &key,
413        ))
414        .unwrap();
415        let recovered = accounts.recover(&signed).unwrap();
416        assert_eq!(recovered, address);
417
418        // these methods make no requests
419        accounts.transport().assert_no_more_requests();
420    }
421
422    #[test]
423    fn sign_transaction_data() {
424        // retrieved test vector from:
425        // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#eth-accounts-signtransaction
426
427        let tx = Transaction {
428            nonce: 0.into(),
429            gas: 2_000_000.into(),
430            gas_price: 234_567_897_654_321u64.into(),
431            to: Some(hex!("F0109fC8DF283027b6285cc889F5aA624EaC1F55").into()),
432            value: 1_000_000_000.into(),
433            data: Vec::new(),
434        };
435        let skey = SecretKey::from_slice(&hex!(
436            "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
437        ))
438        .unwrap();
439        let key = SecretKeyRef::new(&skey);
440
441        let signed = tx.sign(key, 1);
442
443        let expected = SignedTransaction {
444            message_hash: hex!("6893a6ee8df79b0f5d64a180cd1ef35d030f3e296a5361cf04d02ce720d32ec5").into(),
445            v: 0x25,
446            r: hex!("09ebb6ca057a0535d6186462bc0b465b561c94a295bdb0621fc19208ab149a9c").into(),
447            s: hex!("440ffd775ce91a833ab410777204d5341a6f9fa91216a6f3ee2c051fea6a0428").into(),
448            raw_transaction: hex!("f86a8086d55698372431831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a009ebb6ca057a0535d6186462bc0b465b561c94a295bdb0621fc19208ab149a9ca0440ffd775ce91a833ab410777204d5341a6f9fa91216a6f3ee2c051fea6a0428").into(),
449            transaction_hash: hex!("d8f64a42b57be0d565f385378db2f6bf324ce14a594afc05de90436e9ce01f60").into(),
450        };
451
452        assert_eq!(signed, expected);
453    }
454}