pink_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        signing::hash_message(message)
35    }
36}
37
38#[cfg(feature = "signing")]
39mod accounts_signing {
40    use super::*;
41    use crate::prelude::*;
42    use crate::types::{RecoveryMessage, Recovery};
43    use crate::{
44        api::Web3,
45        error,
46        signing::Signature,
47        types::{AccessList, Address, Bytes, SignedData, SignedTransaction, TransactionParameters, U256, U64},
48    };
49    use rlp::RlpStream;
50    use std::convert::TryInto;
51
52    const LEGACY_TX_ID: u64 = 0;
53    const ACCESSLISTS_TX_ID: u64 = 1;
54    const EIP1559_TX_ID: u64 = 2;
55
56    impl<T: Transport> Accounts<T> {
57        /// Gets the parent `web3` namespace
58        fn web3(&self) -> Web3<T> {
59            Web3::new(self.transport.clone())
60        }
61
62        /// Signs an Ethereum transaction with a given private key.
63        ///
64        /// Transaction signing can perform RPC requests in order to fill missing
65        /// parameters required for signing `nonce`, `gas_price` and `chain_id`. Note
66        /// that if all transaction parameters were provided, this future will resolve
67        /// immediately.
68        pub async fn sign_transaction<K: signing::Key>(
69            &self,
70            tx: TransactionParameters,
71            key: K,
72        ) -> error::Result<SignedTransaction> {
73            macro_rules! maybe {
74                ($o: expr, $f: expr) => {
75                    async {
76                        match $o {
77                            Some(value) => Ok(value),
78                            None => $f.await,
79                        }
80                    }
81                };
82            }
83            let from = key.address();
84
85            let gas_price = match tx.transaction_type {
86                Some(tx_type) if tx_type == U64::from(EIP1559_TX_ID) && tx.max_fee_per_gas.is_some() => {
87                    tx.max_fee_per_gas
88                }
89                _ => tx.gas_price,
90            };
91
92            let (nonce, gas_price, chain_id) = futures::future::try_join3(
93                maybe!(tx.nonce, self.web3().eth().transaction_count(from, None)),
94                maybe!(gas_price, self.web3().eth().gas_price()),
95                maybe!(tx.chain_id.map(U256::from), self.web3().eth().chain_id()),
96            )
97            .await?;
98            let chain_id = chain_id.as_u64();
99
100            let max_priority_fee_per_gas = match tx.transaction_type {
101                Some(tx_type) if tx_type == U64::from(EIP1559_TX_ID) => {
102                    tx.max_priority_fee_per_gas.unwrap_or(gas_price)
103                }
104                _ => gas_price,
105            };
106
107            let tx = Transaction {
108                to: tx.to,
109                nonce,
110                gas: tx.gas,
111                gas_price,
112                value: tx.value,
113                data: tx.data.0,
114                transaction_type: tx.transaction_type,
115                access_list: tx.access_list.unwrap_or_default(),
116                max_priority_fee_per_gas,
117            };
118
119            let signed = tx.sign(key, chain_id);
120            Ok(signed)
121        }
122
123        /// Sign arbitrary string data.
124        ///
125        /// The data is UTF-8 encoded and enveloped the same way as with
126        /// `hash_message`. The returned signed data's signature is in 'Electrum'
127        /// notation, that is the recovery value `v` is either `27` or `28` (as
128        /// opposed to the standard notation where `v` is either `0` or `1`). This
129        /// is important to consider when using this signature with other crates.
130        pub fn sign<S>(&self, message: S, key: impl signing::Key) -> SignedData
131        where
132            S: AsRef<[u8]>,
133        {
134            let message = message.as_ref();
135            let message_hash = self.hash_message(message);
136
137            let signature = key
138                .sign(&message_hash.0, None)
139                .expect("hash is non-zero 32-bytes; qed");
140            let v = signature
141                .v
142                .try_into()
143                .expect("signature recovery in electrum notation always fits in a u8");
144
145            let signature_bytes = Bytes({
146                let mut bytes = Vec::with_capacity(65);
147                bytes.extend_from_slice(signature.r.as_bytes());
148                bytes.extend_from_slice(signature.s.as_bytes());
149                bytes.push(v);
150                bytes
151            });
152
153            // We perform this allocation only after all previous fallible actions have completed successfully.
154            let message = message.to_owned();
155
156            SignedData {
157                message,
158                message_hash,
159                v,
160                r: signature.r,
161                s: signature.s,
162                signature: signature_bytes,
163            }
164        }
165
166        /// Recovers the Ethereum address which was used to sign the given data.
167        ///
168        /// Recovery signature data uses 'Electrum' notation, this means the `v`
169        /// value is expected to be either `27` or `28`.
170        pub fn recover<R>(&self, recovery: R) -> error::Result<Address>
171        where
172            R: Into<Recovery>,
173        {
174            let recovery = recovery.into();
175            let message_hash = match recovery.message {
176                RecoveryMessage::Data(ref message) => self.hash_message(message),
177                RecoveryMessage::Hash(hash) => hash,
178            };
179            let (signature, recovery_id) = recovery
180                .as_signature()
181                .ok_or(error::Error::Recovery(signing::RecoveryError::InvalidSignature))?;
182
183            let mut recoverable_signature: [u8; 65] = [0; 65];
184            recoverable_signature[..64].copy_from_slice(&signature[..]);
185            recoverable_signature[64] = recovery_id as u8;
186
187            let mut pub_key = [0; 33];
188            ink_env::ecdsa_recover(&recoverable_signature, message_hash.as_fixed_bytes(), &mut pub_key)
189                .or(Err(error::Error::Recovery(signing::RecoveryError::InvalidSignature)))?;
190
191            let mut address = [0; 20];
192            ink_env::ecdsa_to_eth_address(&pub_key, &mut address)
193                .or(Err(error::Error::Recovery(signing::RecoveryError::InvalidSignature)))?;
194            Ok(address.into())
195        }
196    }
197    /// A transaction used for RLP encoding, hashing and signing.
198    #[derive(Debug)]
199    pub struct Transaction {
200        pub to: Option<Address>,
201        pub nonce: U256,
202        pub gas: U256,
203        pub gas_price: U256,
204        pub value: U256,
205        pub data: Vec<u8>,
206        pub transaction_type: Option<U64>,
207        pub access_list: AccessList,
208        pub max_priority_fee_per_gas: U256,
209    }
210
211    impl Transaction {
212        fn rlp_append_legacy(&self, stream: &mut RlpStream) {
213            stream.append(&self.nonce);
214            stream.append(&self.gas_price);
215            stream.append(&self.gas);
216            if let Some(to) = self.to {
217                stream.append(&to);
218            } else {
219                stream.append(&"");
220            }
221            stream.append(&self.value);
222            stream.append(&self.data);
223        }
224
225        fn encode_legacy(&self, chain_id: u64, signature: Option<&Signature>) -> RlpStream {
226            let mut stream = RlpStream::new();
227            stream.begin_list(9);
228
229            self.rlp_append_legacy(&mut stream);
230
231            if let Some(signature) = signature {
232                self.rlp_append_signature(&mut stream, signature);
233            } else {
234                stream.append(&chain_id);
235                stream.append(&0u8);
236                stream.append(&0u8);
237            }
238
239            stream
240        }
241
242        fn encode_access_list_payload(&self, chain_id: u64, signature: Option<&Signature>) -> RlpStream {
243            let mut stream = RlpStream::new();
244
245            let list_size = if signature.is_some() { 11 } else { 8 };
246            stream.begin_list(list_size);
247
248            // append chain_id. from EIP-2930: chainId is defined to be an integer of arbitrary size.
249            stream.append(&chain_id);
250
251            self.rlp_append_legacy(&mut stream);
252            self.rlp_append_access_list(&mut stream);
253
254            if let Some(signature) = signature {
255                self.rlp_append_signature(&mut stream, signature);
256            }
257
258            stream
259        }
260
261        fn encode_eip1559_payload(&self, chain_id: u64, signature: Option<&Signature>) -> RlpStream {
262            let mut stream = RlpStream::new();
263
264            let list_size = if signature.is_some() { 12 } else { 9 };
265            stream.begin_list(list_size);
266
267            // append chain_id. from EIP-2930: chainId is defined to be an integer of arbitrary size.
268            stream.append(&chain_id);
269
270            stream.append(&self.nonce);
271            stream.append(&self.max_priority_fee_per_gas);
272            stream.append(&self.gas_price);
273            stream.append(&self.gas);
274            if let Some(to) = self.to {
275                stream.append(&to);
276            } else {
277                stream.append(&"");
278            }
279            stream.append(&self.value);
280            stream.append(&self.data);
281
282            self.rlp_append_access_list(&mut stream);
283
284            if let Some(signature) = signature {
285                self.rlp_append_signature(&mut stream, signature);
286            }
287
288            stream
289        }
290
291        fn rlp_append_signature(&self, stream: &mut RlpStream, signature: &Signature) {
292            stream.append(&signature.v);
293            stream.append(&U256::from_big_endian(signature.r.as_bytes()));
294            stream.append(&U256::from_big_endian(signature.s.as_bytes()));
295        }
296
297        fn rlp_append_access_list(&self, stream: &mut RlpStream) {
298            stream.begin_list(self.access_list.len());
299            for access in self.access_list.iter() {
300                stream.begin_list(2);
301                stream.append(&access.address);
302                stream.begin_list(access.storage_keys.len());
303                for storage_key in access.storage_keys.iter() {
304                    stream.append(storage_key);
305                }
306            }
307        }
308
309        fn encode(&self, chain_id: u64, signature: Option<&Signature>) -> Vec<u8> {
310            match self.transaction_type.map(|t| t.as_u64()) {
311                Some(LEGACY_TX_ID) | None => {
312                    let stream = self.encode_legacy(chain_id, signature);
313                    stream.out().to_vec()
314                }
315
316                Some(ACCESSLISTS_TX_ID) => {
317                    let tx_id: u8 = ACCESSLISTS_TX_ID as u8;
318                    let stream = self.encode_access_list_payload(chain_id, signature);
319                    [&[tx_id], stream.as_raw()].concat()
320                }
321
322                Some(EIP1559_TX_ID) => {
323                    let tx_id: u8 = EIP1559_TX_ID as u8;
324                    let stream = self.encode_eip1559_payload(chain_id, signature);
325                    [&[tx_id], stream.as_raw()].concat()
326                }
327
328                _ => {
329                    panic!("Unsupported transaction type");
330                }
331            }
332        }
333
334        /// Sign and return a raw signed transaction.
335        pub fn sign(self, sign: impl signing::Key, chain_id: u64) -> SignedTransaction {
336            let adjust_v_value = matches!(self.transaction_type.map(|t| t.as_u64()), Some(LEGACY_TX_ID) | None);
337
338            let encoded = self.encode(chain_id, None);
339
340            let hash = signing::keccak256(encoded.as_ref());
341
342            let signature = if adjust_v_value {
343                sign.sign(&hash, Some(chain_id))
344                    .expect("hash is non-zero 32-bytes; qed")
345            } else {
346                sign.sign_message(&hash).expect("hash is non-zero 32-bytes; qed")
347            };
348
349            let signed = self.encode(chain_id, Some(&signature));
350            let transaction_hash = signing::keccak256(signed.as_ref()).into();
351
352            SignedTransaction {
353                message_hash: hash.into(),
354                v: signature.v,
355                r: signature.r,
356                s: signature.s,
357                raw_transaction: signed.into(),
358                transaction_hash,
359            }
360        }
361    }
362}
363
364#[cfg(all(test, not(target_arch = "wasm32"), not(feature = "pink")))]
365mod tests {
366    use super::*;
367    use crate::{
368        signing::{SecretKey, SecretKeyRef},
369        transports::test::TestTransport,
370        types::{Address, Recovery, SignedTransaction, TransactionParameters, U256},
371    };
372    use accounts_signing::*;
373    use hex_literal::hex;
374    use serde_json::json;
375
376    #[test]
377    fn accounts_sign_transaction() {
378        // retrieved test vector from:
379        // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
380
381        let tx = TransactionParameters {
382            to: Some(hex!("F0109fC8DF283027b6285cc889F5aA624EaC1F55").into()),
383            value: 1_000_000_000.into(),
384            gas: 2_000_000.into(),
385            ..Default::default()
386        };
387        let key = SecretKey::from_slice(&hex!(
388            "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
389        ))
390        .unwrap();
391        let nonce = U256::zero();
392        let gas_price = U256::from(21_000_000_000u128);
393        let chain_id = "0x1";
394        let from: Address = signing::secret_key_address(&key);
395
396        let mut transport = TestTransport::default();
397        transport.add_response(json!(nonce));
398        transport.add_response(json!(gas_price));
399        transport.add_response(json!(chain_id));
400
401        let signed = {
402            let accounts = Accounts::new(&transport);
403            futures::executor::block_on(accounts.sign_transaction(tx, &key))
404        };
405
406        transport.assert_request(
407            "eth_getTransactionCount",
408            &format!(r#"[{:?}, "latest"]"#, json!(from).to_string()),
409        );
410        transport.assert_request("eth_gasPrice", "[]");
411        transport.assert_request("eth_chainId", "[]");
412        transport.assert_no_more_requests();
413
414        let expected = SignedTransaction {
415            message_hash: hex!("88cfbd7e51c7a40540b233cf68b62ad1df3e92462f1c6018d6d67eae0f3b08f5").into(),
416            v: 0x25,
417            r: hex!("c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895").into(),
418            s: hex!("727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68").into(),
419            raw_transaction: hex!("f869808504e3b29200831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a0c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895a0727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68").into(),
420            transaction_hash: hex!("de8db924885b0803d2edc335f745b2b8750c8848744905684c20b987443a9593").into(),
421        };
422
423        assert_eq!(signed, Ok(expected));
424    }
425
426    #[test]
427    fn accounts_sign_transaction_with_all_parameters() {
428        let key = SecretKey::from_slice(&hex!(
429            "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
430        ))
431        .unwrap();
432
433        let accounts = Accounts::new(TestTransport::default());
434        futures::executor::block_on(accounts.sign_transaction(
435            TransactionParameters {
436                nonce: Some(0.into()),
437                gas_price: Some(1.into()),
438                chain_id: Some(42),
439                ..Default::default()
440            },
441            &key,
442        ))
443        .unwrap();
444
445        // sign_transaction makes no requests when all parameters are specified
446        accounts.transport().assert_no_more_requests();
447    }
448
449    #[test]
450    fn accounts_hash_message() {
451        // test vector taken from:
452        // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#hashmessage
453
454        let accounts = Accounts::new(TestTransport::default());
455        let hash = accounts.hash_message("Hello World");
456
457        assert_eq!(
458            hash,
459            hex!("a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2").into()
460        );
461
462        // this method does not actually make any requests.
463        accounts.transport().assert_no_more_requests();
464    }
465
466    #[test]
467    fn accounts_sign() {
468        // test vector taken from:
469        // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign
470
471        let accounts = Accounts::new(TestTransport::default());
472
473        let key = SecretKey::from_slice(&hex!(
474            "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
475        ))
476        .unwrap();
477        let signed = accounts.sign("Some data", SecretKeyRef::new(&key));
478
479        assert_eq!(
480            signed.message_hash,
481            hex!("1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655").into()
482        );
483        assert_eq!(
484            signed.signature.0,
485            hex!("b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c")
486        );
487
488        // this method does not actually make any requests.
489        accounts.transport().assert_no_more_requests();
490    }
491
492    #[test]
493    fn accounts_recover() {
494        // test vector taken from:
495        // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#recover
496
497        let accounts = Accounts::new(TestTransport::default());
498
499        let v = 0x1cu64;
500        let r = hex!("b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd").into();
501        let s = hex!("6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029").into();
502
503        let recovery = Recovery::new("Some data", v, r, s);
504        assert_eq!(
505            accounts.recover(recovery).unwrap(),
506            hex!("2c7536E3605D9C16a7a3D7b1898e529396a65c23").into()
507        );
508
509        // this method does not actually make any requests.
510        accounts.transport().assert_no_more_requests();
511    }
512
513    #[test]
514    fn accounts_recover_signed() {
515        let key = SecretKey::from_slice(&hex!(
516            "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
517        ))
518        .unwrap();
519        let address: Address = signing::secret_key_address(&key);
520
521        let accounts = Accounts::new(TestTransport::default());
522
523        let signed = accounts.sign("rust-web3 rocks!", &key);
524        let recovered = accounts.recover(&signed).unwrap();
525        assert_eq!(recovered, address);
526
527        let signed = futures::executor::block_on(accounts.sign_transaction(
528            TransactionParameters {
529                nonce: Some(0.into()),
530                gas_price: Some(1u128.into()),
531                chain_id: Some(42),
532                ..Default::default()
533            },
534            &key,
535        ))
536        .unwrap();
537        let recovered = accounts.recover(&signed).unwrap();
538        assert_eq!(recovered, address);
539
540        // these methods make no requests
541        accounts.transport().assert_no_more_requests();
542    }
543
544    #[test]
545    fn sign_transaction_data() {
546        // retrieved test vector from:
547        // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#eth-accounts-signtransaction
548
549        let tx = Transaction {
550            nonce: 0u128.into(),
551            gas: 2_000_000u128.into(),
552            gas_price: 234_567_897_654_321u64.into(),
553            to: Some(hex!("F0109fC8DF283027b6285cc889F5aA624EaC1F55").into()),
554            value: 1_000_000_000u128.into(),
555            data: Vec::new(),
556            transaction_type: None,
557            access_list: vec![],
558            max_priority_fee_per_gas: 0u128.into(),
559        };
560        let skey = SecretKey::from_slice(&hex!(
561            "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
562        ))
563        .unwrap();
564        let key = SecretKeyRef::new(&skey);
565
566        let signed = tx.sign(key, 1);
567
568        let expected = SignedTransaction {
569            message_hash: hex!("6893a6ee8df79b0f5d64a180cd1ef35d030f3e296a5361cf04d02ce720d32ec5").into(),
570            v: 0x25,
571            r: hex!("09ebb6ca057a0535d6186462bc0b465b561c94a295bdb0621fc19208ab149a9c").into(),
572            s: hex!("440ffd775ce91a833ab410777204d5341a6f9fa91216a6f3ee2c051fea6a0428").into(),
573            raw_transaction: hex!("f86a8086d55698372431831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a009ebb6ca057a0535d6186462bc0b465b561c94a295bdb0621fc19208ab149a9ca0440ffd775ce91a833ab410777204d5341a6f9fa91216a6f3ee2c051fea6a0428").into(),
574            transaction_hash: hex!("d8f64a42b57be0d565f385378db2f6bf324ce14a594afc05de90436e9ce01f60").into(),
575        };
576
577        assert_eq!(signed, expected);
578    }
579}