1use crate::{api::Namespace, signing, types::H256, Transport};
4use crate::ic::{KeyInfo, ic_raw_sign, recover_address};
5
6#[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 pub fn hash_message<S>(&self, message: S) -> H256
32 where
33 S: AsRef<[u8]>,
34 {
35 signing::hash_message(message)
36 }
37}
38
39mod 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 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 fn web3(&self) -> Web3<T> {
61 Web3::new(self.transport.clone())
62 }
63
64 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 }
225 #[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 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 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 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 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 accounts.transport().assert_no_more_requests();
518 }
519
520 #[test]
521 fn accounts_hash_message() {
522 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 accounts.transport().assert_no_more_requests();
535 }
536
537 #[test]
538 fn accounts_sign() {
539 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 accounts.transport().assert_no_more_requests();
561 }
562
563 #[test]
564 fn accounts_recover() {
565 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 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 accounts.transport().assert_no_more_requests();
613 }
614
615 #[test]
616 fn sign_transaction_data() {
617 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}