1use crate::{api::Namespace, signing, types::H256, Transport};
4
5#[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 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 fn web3(&self) -> Web3<T> {
59 Web3::new(self.transport.clone())
60 }
61
62 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 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 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 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 #[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 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 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 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 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 accounts.transport().assert_no_more_requests();
447 }
448
449 #[test]
450 fn accounts_hash_message() {
451 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 accounts.transport().assert_no_more_requests();
464 }
465
466 #[test]
467 fn accounts_sign() {
468 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 accounts.transport().assert_no_more_requests();
490 }
491
492 #[test]
493 fn accounts_recover() {
494 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 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 accounts.transport().assert_no_more_requests();
542 }
543
544 #[test]
545 fn sign_transaction_data() {
546 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}