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