ic_web3/api/
accounts.rs

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