bitbox_api/
eth.rs

1use crate::runtime::Runtime;
2
3use crate::error::Error;
4use crate::pb::{
5    self,
6    eth_sign_typed_message_request::{DataType, Member, MemberType, StructType},
7    eth_typed_message_value_response::RootObject,
8    request::Request,
9    response::Response,
10};
11use crate::Keypath;
12use crate::PairedBitBox;
13
14use std::collections::HashMap;
15use std::str::FromStr;
16
17use num_bigint::{BigInt, BigUint};
18//use num_traits::ToPrimitive;
19use serde_json::Value;
20
21impl<R: Runtime> PairedBitBox<R> {
22    async fn query_proto_eth(
23        &self,
24        request: pb::eth_request::Request,
25    ) -> Result<pb::eth_response::Response, Error> {
26        match self
27            .query_proto(Request::Eth(pb::EthRequest {
28                request: Some(request),
29            }))
30            .await?
31        {
32            Response::Eth(pb::EthResponse {
33                response: Some(response),
34            }) => Ok(response),
35            _ => Err(Error::UnexpectedResponse),
36        }
37    }
38
39    /// Does this device support ETH functionality? Currently this means BitBox02 Multi.
40    pub fn eth_supported(&self) -> bool {
41        self.is_multi_edition()
42    }
43
44    /// Query the device for an xpub.
45    pub async fn eth_xpub(&self, keypath: &Keypath) -> Result<String, Error> {
46        match self
47            .query_proto_eth(pb::eth_request::Request::Pub(pb::EthPubRequest {
48                keypath: keypath.to_vec(),
49                coin: 0,
50                output_type: pb::eth_pub_request::OutputType::Xpub as _,
51                display: false,
52                contract_address: vec![],
53                chain_id: 0,
54            }))
55            .await?
56        {
57            pb::eth_response::Response::Pub(pb::PubResponse { r#pub }) => Ok(r#pub),
58            _ => Err(Error::UnexpectedResponse),
59        }
60    }
61
62    /// Query the device for an Ethereum address.
63    pub async fn eth_address(
64        &self,
65        chain_id: u64,
66        keypath: &Keypath,
67        display: bool,
68    ) -> Result<String, Error> {
69        match self
70            .query_proto_eth(pb::eth_request::Request::Pub(pb::EthPubRequest {
71                keypath: keypath.to_vec(),
72                coin: 0,
73                output_type: pb::eth_pub_request::OutputType::Address as _,
74                display,
75                contract_address: vec![],
76                chain_id,
77            }))
78            .await?
79        {
80            pb::eth_response::Response::Pub(pb::PubResponse { r#pub }) => Ok(r#pub),
81            _ => Err(Error::UnexpectedResponse),
82        }
83    }
84}
85
86#[cfg_attr(
87    feature = "wasm",
88    derive(serde::Deserialize),
89    serde(rename_all = "camelCase")
90)]
91pub struct Transaction {
92    /// Nonce must be big-endian encoded, no trailing zeroes.
93    pub nonce: Vec<u8>,
94    /// Gas price must be big-endian encoded, no trailing zeroes.
95    pub gas_price: Vec<u8>,
96    /// Gas limit must be big-endian encoded, no trailing zeroes.
97    pub gas_limit: Vec<u8>,
98    pub recipient: [u8; 20],
99    /// Value must be big-endian encoded, no trailing zeroes.
100    pub value: Vec<u8>,
101    pub data: Vec<u8>,
102}
103
104#[cfg_attr(
105    feature = "wasm",
106    derive(serde::Deserialize),
107    serde(rename_all = "camelCase")
108)]
109pub struct EIP1559Transaction {
110    pub chain_id: u64,
111    /// Nonce must be big-endian encoded, no trailing zeroes.
112    pub nonce: Vec<u8>,
113    /// Max priority fee must be big-endian encoded, no trailing zeroes.
114    pub max_priority_fee_per_gas: Vec<u8>,
115    /// max fee must be big-endian encoded, no trailing zeroes.
116    pub max_fee_per_gas: Vec<u8>,
117    /// Gas limit must be big-endian encoded, no trailing zeroes.
118    pub gas_limit: Vec<u8>,
119    pub recipient: [u8; 20],
120    /// Value must be big-endian encoded, no trailing zeroes.
121    pub value: Vec<u8>,
122    pub data: Vec<u8>,
123}
124
125/// Identifies the case of the recipient address given as hexadecimal string.
126/// This function exists as a convenience to help clients to determine the case of the
127/// recipient address.
128pub fn eth_identify_case(recipient_address: &str) -> pb::EthAddressCase {
129    if recipient_address
130        .chars()
131        .all(|c| !c.is_ascii_alphabetic() || c.is_ascii_uppercase())
132    {
133        pb::EthAddressCase::Upper
134    } else if recipient_address
135        .chars()
136        .all(|c| !c.is_ascii_alphabetic() || c.is_ascii_lowercase())
137    {
138        pb::EthAddressCase::Lower
139    } else {
140        pb::EthAddressCase::Mixed
141    }
142}
143
144#[cfg(feature = "rlp")]
145impl TryFrom<&[u8]> for Transaction {
146    type Error = ();
147
148    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
149        let [nonce, gas_price, gas_limit, recipient, value, data, _, _, _]: [Vec<u8>; 9] =
150            rlp::decode_list(value).try_into().map_err(|_| ())?;
151        Ok(Transaction {
152            nonce,
153            gas_price,
154            gas_limit,
155            recipient: recipient.try_into().map_err(|_| ())?,
156            value,
157            data,
158        })
159    }
160}
161
162#[cfg(feature = "rlp")]
163impl TryFrom<&[u8]> for EIP1559Transaction {
164    type Error = ();
165
166    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
167        let [mut chain_id_vec, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, recipient, value, data, _, _, _]: [Vec<u8>; 11] =
168            rlp::decode_list(value).try_into().map_err(|_| ())?;
169        while chain_id_vec.len() < 8 {
170            chain_id_vec.insert(0, 0);
171        }
172        let chain_id = u64::from_be_bytes(chain_id_vec.try_into().map_err(|_| ())?);
173        Ok(EIP1559Transaction {
174            chain_id,
175            nonce,
176            max_priority_fee_per_gas,
177            max_fee_per_gas,
178            gas_limit,
179            recipient: recipient.try_into().map_err(|_| ())?,
180            value,
181            data,
182        })
183    }
184}
185
186#[derive(Debug, PartialEq, serde::Deserialize)]
187struct Eip712TypeMember {
188    name: String,
189    r#type: String,
190}
191
192#[derive(Debug, PartialEq, serde::Deserialize)]
193#[serde(rename_all = "camelCase")]
194struct Eip712Message {
195    types: HashMap<String, Vec<Eip712TypeMember>>,
196    primary_type: String,
197    domain: HashMap<String, Value>,
198    message: HashMap<String, Value>,
199}
200
201fn parse_type(
202    typ: &str,
203    types: &HashMap<String, Vec<Eip712TypeMember>>,
204) -> Result<MemberType, String> {
205    if typ.ends_with(']') {
206        let index = typ
207            .rfind('[')
208            .ok_or(format!("Invalid type format: {typ}"))?;
209        let (rest, size) = (&typ[..index], &typ[index + 1..typ.len() - 1]);
210        let size_int = if !size.is_empty() {
211            u32::from_str(size).map_err(|e| format!("Error parsing size: {e}"))?
212        } else {
213            0
214        };
215        let array_type = Box::new(parse_type(rest, types)?);
216        Ok(MemberType {
217            r#type: DataType::Array.into(),
218            size: size_int,
219            struct_name: String::new(),
220            array_type: Some(array_type),
221        })
222    } else if let Some(size) = typ.strip_prefix("bytes") {
223        let size_int = if !size.is_empty() {
224            u32::from_str(size).map_err(|e| format!("Error parsing size: {e}"))?
225        } else {
226            0
227        };
228        Ok(MemberType {
229            r#type: DataType::Bytes.into(),
230            size: size_int,
231            struct_name: String::new(),
232            array_type: None,
233        })
234    } else if let Some(size) = typ.strip_prefix("uint") {
235        if size.is_empty() {
236            return Err("uint must be sized".to_string());
237        }
238        let size_int = u32::from_str(size).map_err(|e| format!("Error parsing size: {e}"))? / 8;
239        Ok(MemberType {
240            r#type: DataType::Uint.into(),
241            size: size_int,
242            struct_name: String::new(),
243            array_type: None,
244        })
245    } else if let Some(size) = typ.strip_prefix("int") {
246        if size.is_empty() {
247            return Err("int must be sized".to_string());
248        }
249        let size_int = u32::from_str(size).map_err(|e| format!("Error parsing size: {e}"))? / 8;
250        Ok(MemberType {
251            r#type: DataType::Int.into(),
252            size: size_int,
253            struct_name: String::new(),
254            array_type: None,
255        })
256    } else if typ == "bool" {
257        Ok(MemberType {
258            r#type: DataType::Bool.into(),
259            size: 0,
260            struct_name: String::new(),
261            array_type: None,
262        })
263    } else if typ == "address" {
264        Ok(MemberType {
265            r#type: DataType::Address.into(),
266            size: 0,
267            struct_name: String::new(),
268            array_type: None,
269        })
270    } else if typ == "string" {
271        Ok(MemberType {
272            r#type: DataType::String.into(),
273            size: 0,
274            struct_name: String::new(),
275            array_type: None,
276        })
277    } else if types.contains_key(typ) {
278        Ok(MemberType {
279            r#type: DataType::Struct.into(),
280            size: 0,
281            struct_name: typ.to_string(),
282            array_type: None,
283        })
284    } else {
285        Err(format!("Can't recognize type: {typ}"))
286    }
287}
288
289fn encode_value(typ: &MemberType, value: &Value) -> Result<Vec<u8>, String> {
290    match DataType::try_from(typ.r#type).unwrap() {
291        DataType::Bytes => {
292            if let Value::String(v) = value {
293                if v.starts_with("0x") || v.starts_with("0X") {
294                    hex::decode(&v[2..]).map_err(|e| e.to_string())
295                } else {
296                    Ok(v.as_bytes().to_vec())
297                }
298            } else {
299                Err("Expected a string for bytes type".to_string())
300            }
301        }
302        DataType::Uint => match value {
303            Value::String(v) => {
304                if v.starts_with("0x") || v.starts_with("0X") {
305                    Ok(BigUint::parse_bytes(&v.as_bytes()[2..], 16)
306                        .ok_or(format!("could not parse {v} as hex"))?
307                        .to_bytes_be())
308                } else {
309                    Ok(BigUint::from_str(v)
310                        .map_err(|e| e.to_string())?
311                        .to_bytes_be())
312                }
313            }
314            Value::Number(n) => {
315                if let Some(v) = n.as_f64() {
316                    let v64: u64 = v as _;
317                    if (v64 as f64) != v {
318                        Err("Number is not an uint".to_string())
319                    } else {
320                        Ok(BigUint::from(v64).to_bytes_be())
321                    }
322                } else {
323                    Err("Number is not an uint".to_string())
324                }
325            }
326            _ => Err("Wrong type for uint".to_string()),
327        },
328        DataType::Int => match value {
329            Value::String(v) => Ok(BigInt::from_str(v)
330                .map_err(|e| e.to_string())?
331                .to_signed_bytes_be()),
332            Value::Number(n) => {
333                if let Some(v) = n.as_f64() {
334                    let v64: i64 = v as _;
335                    if (v64 as f64) != v {
336                        Err("Number is not an int".to_string())
337                    } else {
338                        let mut bytes = BigInt::from(v64).to_signed_bytes_be();
339                        // Drop leading zero. There can be at most one.
340                        if let [0, ..] = bytes.as_slice() {
341                            bytes.remove(0);
342                        }
343                        Ok(bytes)
344                    }
345                } else {
346                    Err("Number is not an int".to_string())
347                }
348            }
349            _ => Err("Wrong type for int".to_string()),
350        },
351        DataType::Bool => {
352            if value.as_bool().ok_or("Expected a boolean value")? {
353                Ok(vec![1])
354            } else {
355                Ok(vec![0])
356            }
357        }
358        DataType::Address | DataType::String => {
359            if let Value::String(v) = value {
360                Ok(v.as_bytes().to_vec())
361            } else {
362                Err("Expected a string value".to_string())
363            }
364        }
365        DataType::Array => {
366            if let Value::Array(arr) = value {
367                let size = arr.len() as u32;
368                Ok(size.to_be_bytes().to_vec())
369            } else {
370                Err("Expected an array".to_string())
371            }
372        }
373        _ => Err("looked up value must not be a map or array".to_string()),
374    }
375}
376
377fn get_value(
378    what: &pb::EthTypedMessageValueResponse,
379    msg: &Eip712Message,
380) -> Result<Vec<u8>, String> {
381    enum Either<'a> {
382        HashMap(&'a HashMap<String, Value>),
383        JsonValue(Value),
384    }
385    impl Either<'_> {
386        fn get(&self, key: &str) -> Option<&Value> {
387            match self {
388                Either::HashMap(map) => map.get(key),
389                Either::JsonValue(Value::Object(map)) => map.get(key),
390                _ => None,
391            }
392        }
393    }
394
395    let (mut value, mut typ): (Either, MemberType) =
396        match RootObject::try_from(what.root_object).unwrap() {
397            RootObject::Unknown => return Err("unkown root object".into()),
398            RootObject::Domain => (
399                Either::HashMap(&msg.domain),
400                parse_type("EIP712Domain", &msg.types)?,
401            ),
402            RootObject::Message => (
403                Either::HashMap(&msg.message),
404                parse_type(&msg.primary_type, &msg.types)?,
405            ),
406        };
407    for element in what.path.iter() {
408        match DataType::try_from(typ.r#type).unwrap() {
409            DataType::Struct => {
410                let struct_member: &Eip712TypeMember = msg
411                    .types
412                    .get(&typ.struct_name)
413                    .ok_or(format!(
414                        "could not lookup type of name: {}",
415                        &typ.struct_name
416                    ))?
417                    .get(*element as usize)
418                    .ok_or(format!(
419                        "could not lookup member #{} of type: {}",
420                        *element, &typ.struct_name
421                    ))?;
422                value = Either::JsonValue(
423                    value
424                        .get(&struct_member.name)
425                        .ok_or(format!("could not lookup: {}", struct_member.name.as_str()))?
426                        .clone(),
427                );
428                typ = parse_type(&struct_member.r#type, &msg.types)?;
429            }
430            DataType::Array => {
431                if let Either::JsonValue(Value::Array(list)) = value {
432                    value = Either::JsonValue(
433                        list.get(*element as usize)
434                            .ok_or(format!("could not lookup array index: {}", *element))?
435                            .clone(),
436                    );
437                    typ = *typ.array_type.unwrap();
438                }
439            }
440            _ => return Err("path element does not point to struct or array".into()),
441        }
442    }
443    if let Either::JsonValue(value) = &value {
444        encode_value(&typ, value)
445    } else {
446        Err("path points to struct or array; value expected".to_string())
447    }
448}
449
450impl<R: Runtime> PairedBitBox<R> {
451    async fn handle_antiklepto(
452        &self,
453        response: &pb::eth_response::Response,
454        host_nonce: [u8; 32],
455    ) -> Result<[u8; 65], Error> {
456        match response {
457            pb::eth_response::Response::AntikleptoSignerCommitment(
458                pb::AntiKleptoSignerCommitment { commitment },
459            ) => {
460                match self
461                    .query_proto_eth(pb::eth_request::Request::AntikleptoSignature(
462                        pb::AntiKleptoSignatureRequest {
463                            host_nonce: host_nonce.to_vec(),
464                        },
465                    ))
466                    .await?
467                {
468                    pb::eth_response::Response::Sign(pb::EthSignResponse { signature }) => {
469                        crate::antiklepto::verify_ecdsa(&host_nonce, commitment, &signature)?;
470                        signature.try_into().map_err(|_| Error::UnexpectedResponse)
471                    }
472                    _ => Err(Error::UnexpectedResponse),
473                }
474            }
475            _ => Err(Error::UnexpectedResponse),
476        }
477    }
478
479    /// Signs an Ethereum transaction. It returns a 65 byte signature (R, S, and 1 byte recID).  The
480    /// `tx` param can be constructed manually or parsed from a raw transaction using
481    /// `raw_tx_slice.try_into()` (`rlp` feature required).
482    pub async fn eth_sign_transaction(
483        &self,
484        chain_id: u64,
485        keypath: &Keypath,
486        tx: &Transaction,
487        address_case: Option<pb::EthAddressCase>,
488    ) -> Result<[u8; 65], Error> {
489        // passing chainID instead of coin only since v9.10.0
490        self.validate_version(">=9.10.0")?;
491
492        let host_nonce = crate::antiklepto::gen_host_nonce()?;
493        let request = pb::eth_request::Request::Sign(pb::EthSignRequest {
494            coin: 0,
495            keypath: keypath.to_vec(),
496            nonce: crate::util::remove_leading_zeroes(&tx.nonce),
497            gas_price: crate::util::remove_leading_zeroes(&tx.gas_price),
498            gas_limit: crate::util::remove_leading_zeroes(&tx.gas_limit),
499            recipient: tx.recipient.to_vec(),
500            value: crate::util::remove_leading_zeroes(&tx.value),
501            data: tx.data.clone(),
502            host_nonce_commitment: Some(pb::AntiKleptoHostNonceCommitment {
503                commitment: crate::antiklepto::host_commit(&host_nonce).to_vec(),
504            }),
505            chain_id,
506            address_case: address_case.unwrap_or(pb::EthAddressCase::Mixed).into(),
507        });
508        let response = self.query_proto_eth(request).await?;
509        self.handle_antiklepto(&response, host_nonce).await
510    }
511
512    /// Signs an Ethereum type 2 transaction according to EIP 1559. It returns a 65 byte signature (R, S, and 1 byte recID).
513    /// The `tx` param can be constructed manually or parsed from a raw transaction using
514    /// `raw_tx_slice.try_into()` (`rlp` feature required).
515    pub async fn eth_sign_1559_transaction(
516        &self,
517        keypath: &Keypath,
518        tx: &EIP1559Transaction,
519        address_case: Option<pb::EthAddressCase>,
520    ) -> Result<[u8; 65], Error> {
521        // EIP1559 is suported from v9.16.0
522        self.validate_version(">=9.16.0")?;
523
524        let host_nonce = crate::antiklepto::gen_host_nonce()?;
525        let request = pb::eth_request::Request::SignEip1559(pb::EthSignEip1559Request {
526            chain_id: tx.chain_id,
527            keypath: keypath.to_vec(),
528            nonce: crate::util::remove_leading_zeroes(&tx.nonce),
529            max_priority_fee_per_gas: crate::util::remove_leading_zeroes(
530                &tx.max_priority_fee_per_gas,
531            ),
532            max_fee_per_gas: crate::util::remove_leading_zeroes(&tx.max_fee_per_gas),
533            gas_limit: crate::util::remove_leading_zeroes(&tx.gas_limit),
534            recipient: tx.recipient.to_vec(),
535            value: crate::util::remove_leading_zeroes(&tx.value),
536            data: tx.data.clone(),
537            host_nonce_commitment: Some(pb::AntiKleptoHostNonceCommitment {
538                commitment: crate::antiklepto::host_commit(&host_nonce).to_vec(),
539            }),
540            address_case: address_case.unwrap_or(pb::EthAddressCase::Mixed).into(),
541        });
542        let response = self.query_proto_eth(request).await?;
543        self.handle_antiklepto(&response, host_nonce).await
544    }
545
546    /// Signs an Ethereum message. The provided msg will be prefixed with "\x19Ethereum message\n" +
547    /// len(msg) in the hardware, e.g. "\x19Ethereum\n5hello" (yes, the len prefix is the ascii
548    /// representation with no fixed size or delimiter).  It returns a 65 byte signature (R, S, and
549    /// 1 byte recID). 27 is added to the recID to denote an uncompressed pubkey.
550    pub async fn eth_sign_message(
551        &self,
552        chain_id: u64,
553        keypath: &Keypath,
554        msg: &[u8],
555    ) -> Result<[u8; 65], Error> {
556        // passing chainID instead of coin only since v9.10.0
557        self.validate_version(">=9.10.0")?;
558
559        let host_nonce = crate::antiklepto::gen_host_nonce()?;
560        let request = pb::eth_request::Request::SignMsg(pb::EthSignMessageRequest {
561            coin: 0,
562            keypath: keypath.to_vec(),
563            msg: msg.to_vec(),
564            host_nonce_commitment: Some(pb::AntiKleptoHostNonceCommitment {
565                commitment: crate::antiklepto::host_commit(&host_nonce).to_vec(),
566            }),
567            chain_id,
568        });
569        let response = self.query_proto_eth(request).await?;
570        let mut signature = self.handle_antiklepto(&response, host_nonce).await?;
571        // 27 is the magic constant to add to the recoverable ID to denote an uncompressed pubkey.
572        signature[64] += 27;
573        Ok(signature)
574    }
575
576    /// Signs an Ethereum EIP-712 typed message. It returns a 65 byte signature (R, S, and 1 byte
577    /// recID). 27 is added to the recID to denote an uncompressed pubkey.
578    pub async fn eth_sign_typed_message(
579        &self,
580        chain_id: u64,
581        keypath: &Keypath,
582        json_msg: &str,
583    ) -> Result<[u8; 65], Error> {
584        self.validate_version(">=9.12.0")?;
585
586        let msg: Eip712Message = serde_json::from_str(json_msg)
587            .map_err(|_| Error::EthTypedMessage("Could not parse EIP-712 JSON message".into()))?;
588
589        let parsed_types: Vec<StructType> = msg
590            .types
591            .iter()
592            .map(|(name, members)| {
593                Ok(StructType {
594                    name: name.clone(),
595                    members: members
596                        .iter()
597                        .map(|member| {
598                            Ok(Member {
599                                name: member.name.clone(),
600                                r#type: Some(parse_type(&member.r#type, &msg.types)?),
601                            })
602                        })
603                        .collect::<Result<Vec<Member>, String>>()?,
604                })
605            })
606            .collect::<Result<Vec<StructType>, String>>()
607            .map_err(Error::EthTypedMessage)?;
608
609        let host_nonce = crate::antiklepto::gen_host_nonce()?;
610
611        let mut response = self
612            .query_proto_eth(pb::eth_request::Request::SignTypedMsg(
613                pb::EthSignTypedMessageRequest {
614                    chain_id,
615                    keypath: keypath.to_vec(),
616                    types: parsed_types,
617                    primary_type: msg.primary_type.clone(),
618                    host_nonce_commitment: Some(pb::AntiKleptoHostNonceCommitment {
619                        commitment: crate::antiklepto::host_commit(&host_nonce).to_vec(),
620                    }),
621                },
622            ))
623            .await?;
624        while let pb::eth_response::Response::TypedMsgValue(typed_msg_value) = &response {
625            let value = get_value(typed_msg_value, &msg).map_err(Error::EthTypedMessage)?;
626            response = self
627                .query_proto_eth(pb::eth_request::Request::TypedMsgValue(
628                    pb::EthTypedMessageValueRequest { value },
629                ))
630                .await?;
631        }
632        let mut signature = self.handle_antiklepto(&response, host_nonce).await?;
633        // 27 is the magic constant to add to the recoverable ID to denote an uncompressed pubkey.
634        signature[64] += 27;
635        Ok(signature)
636    }
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642
643    const EIP712_MSG: &str = r#"
644        {
645    "types": {
646        "EIP712Domain": [
647            { "name": "name", "type": "string" },
648            { "name": "version", "type": "string" },
649            { "name": "chainId", "type": "uint256" },
650            { "name": "verifyingContract", "type": "address" }
651        ],
652        "Attachment": [
653            { "name": "contents", "type": "string" }
654        ],
655        "Person": [
656            { "name": "name", "type": "string" },
657            { "name": "wallet", "type": "address" },
658            { "name": "age", "type": "uint8" }
659        ],
660        "Mail": [
661            { "name": "from", "type": "Person" },
662            { "name": "to", "type": "Person" },
663            { "name": "contents", "type": "string" },
664            { "name": "attachments", "type": "Attachment[]" }
665        ]
666    },
667    "primaryType": "Mail",
668    "domain": {
669        "name": "Ether Mail",
670        "version": "1",
671        "chainId": 1,
672        "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
673    },
674    "message": {
675        "from": {
676            "name": "Cow",
677            "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
678            "age": 20
679        },
680        "to": {
681            "name": "Bob",
682            "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
683            "age": "0x1e"
684        },
685        "contents": "Hello, Bob!",
686        "attachments": [{ "contents": "attachment1" }, { "contents": "attachment2" }]
687    }
688}
689    "#;
690
691    fn map_from(elements: &[(&str, Value)]) -> Value {
692        let mut m = serde_json::Map::<String, Value>::new();
693        for (k, v) in elements.iter().cloned() {
694            m.insert(k.into(), v);
695        }
696        m.into()
697    }
698
699    #[test]
700    fn test_deserialize_eip713_message() {
701        let msg: Eip712Message = serde_json::from_str(EIP712_MSG).unwrap();
702        assert_eq!(
703            msg,
704            Eip712Message {
705                types: HashMap::from([
706                    (
707                        "EIP712Domain".into(),
708                        vec![
709                            Eip712TypeMember {
710                                name: "name".into(),
711                                r#type: "string".into()
712                            },
713                            Eip712TypeMember {
714                                name: "version".into(),
715                                r#type: "string".into()
716                            },
717                            Eip712TypeMember {
718                                name: "chainId".into(),
719                                r#type: "uint256".into()
720                            },
721                            Eip712TypeMember {
722                                name: "verifyingContract".into(),
723                                r#type: "address".into()
724                            },
725                        ]
726                    ),
727                    (
728                        "Attachment".into(),
729                        vec![Eip712TypeMember {
730                            name: "contents".into(),
731                            r#type: "string".into()
732                        },]
733                    ),
734                    (
735                        "Person".into(),
736                        vec![
737                            Eip712TypeMember {
738                                name: "name".into(),
739                                r#type: "string".into()
740                            },
741                            Eip712TypeMember {
742                                name: "wallet".into(),
743                                r#type: "address".into()
744                            },
745                            Eip712TypeMember {
746                                name: "age".into(),
747                                r#type: "uint8".into()
748                            },
749                        ]
750                    ),
751                    (
752                        "Mail".into(),
753                        vec![
754                            Eip712TypeMember {
755                                name: "from".into(),
756                                r#type: "Person".into()
757                            },
758                            Eip712TypeMember {
759                                name: "to".into(),
760                                r#type: "Person".into()
761                            },
762                            Eip712TypeMember {
763                                name: "contents".into(),
764                                r#type: "string".into()
765                            },
766                            Eip712TypeMember {
767                                name: "attachments".into(),
768                                r#type: "Attachment[]".into()
769                            },
770                        ]
771                    ),
772                ]),
773                primary_type: "Mail".into(),
774                domain: HashMap::from([
775                    ("name".into(), "Ether Mail".into()),
776                    ("version".into(), "1".into()),
777                    ("chainId".into(), 1.into()),
778                    (
779                        "verifyingContract".into(),
780                        "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC".into()
781                    ),
782                ]),
783                message: HashMap::from([
784                    (
785                        "from".into(),
786                        map_from(&[
787                            ("name", "Cow".into()),
788                            (
789                                "wallet",
790                                "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826".into()
791                            ),
792                            ("age", 20.into())
793                        ]),
794                    ),
795                    (
796                        "to".into(),
797                        map_from(&[
798                            ("name", "Bob".into()),
799                            (
800                                "wallet",
801                                "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB".into()
802                            ),
803                            ("age", "0x1e".into())
804                        ]),
805                    ),
806                    ("contents".into(), "Hello, Bob!".into()),
807                    (
808                        "attachments".into(),
809                        Value::Array(vec![
810                            map_from(&[("contents", "attachment1".into())]),
811                            map_from(&[("contents", "attachment2".into())])
812                        ])
813                    ),
814                ]),
815            }
816        );
817    }
818
819    fn parse_type_no_err(typ: &str, types: &HashMap<String, Vec<Eip712TypeMember>>) -> MemberType {
820        parse_type(typ, types).unwrap()
821    }
822
823    #[test]
824    fn test_parse_type() {
825        assert_eq!(
826            MemberType {
827                r#type: DataType::String.into(),
828                ..Default::default()
829            },
830            parse_type_no_err("string", &HashMap::new())
831        );
832
833        // Bytes.
834        assert_eq!(
835            MemberType {
836                r#type: DataType::Bytes.into(),
837                ..Default::default()
838            },
839            parse_type_no_err("bytes", &HashMap::new())
840        );
841        assert_eq!(
842            MemberType {
843                r#type: DataType::Bytes.into(),
844                size: 1,
845                ..Default::default()
846            },
847            parse_type_no_err("bytes1", &HashMap::new())
848        );
849        assert_eq!(
850            MemberType {
851                r#type: DataType::Bytes.into(),
852                size: 10,
853                ..Default::default()
854            },
855            parse_type_no_err("bytes10", &HashMap::new())
856        );
857        assert_eq!(
858            MemberType {
859                r#type: DataType::Bytes.into(),
860                size: 32,
861                ..Default::default()
862            },
863            parse_type_no_err("bytes32", &HashMap::new())
864        );
865
866        assert_eq!(
867            MemberType {
868                r#type: DataType::Bool.into(),
869                ..Default::default()
870            },
871            parse_type_no_err("bool", &HashMap::new())
872        );
873
874        assert_eq!(
875            MemberType {
876                r#type: DataType::Address.into(),
877                ..Default::default()
878            },
879            parse_type_no_err("address", &HashMap::new())
880        );
881
882        // Uints.
883        assert_eq!(
884            MemberType {
885                r#type: DataType::Uint.into(),
886                size: 1,
887                ..Default::default()
888            },
889            parse_type_no_err("uint8", &HashMap::new())
890        );
891        assert_eq!(
892            MemberType {
893                r#type: DataType::Uint.into(),
894                size: 2,
895                ..Default::default()
896            },
897            parse_type_no_err("uint16", &HashMap::new())
898        );
899        assert_eq!(
900            MemberType {
901                r#type: DataType::Uint.into(),
902                size: 32,
903                ..Default::default()
904            },
905            parse_type_no_err("uint256", &HashMap::new())
906        );
907        assert!(parse_type("uint", &HashMap::new()).is_err());
908        assert!(parse_type("uintfoo", &HashMap::new()).is_err());
909
910        // Ints.
911        assert_eq!(
912            MemberType {
913                r#type: DataType::Int.into(),
914                size: 1,
915                ..Default::default()
916            },
917            parse_type_no_err("int8", &HashMap::new())
918        );
919        assert_eq!(
920            MemberType {
921                r#type: DataType::Int.into(),
922                size: 2,
923                ..Default::default()
924            },
925            parse_type_no_err("int16", &HashMap::new())
926        );
927        assert_eq!(
928            MemberType {
929                r#type: DataType::Int.into(),
930                size: 32,
931                ..Default::default()
932            },
933            parse_type_no_err("int256", &HashMap::new())
934        );
935        assert!(parse_type("int", &HashMap::new()).is_err());
936        assert!(parse_type("intfoo", &HashMap::new()).is_err());
937
938        // Arrays.
939        assert_eq!(
940            MemberType {
941                r#type: DataType::Array.into(),
942                array_type: Some(Box::new(MemberType {
943                    r#type: DataType::String.into(),
944                    ..Default::default()
945                })),
946                ..Default::default()
947            },
948            parse_type_no_err("string[]", &HashMap::new())
949        );
950        assert_eq!(
951            MemberType {
952                r#type: DataType::Array.into(),
953                size: 521,
954                array_type: Some(Box::new(MemberType {
955                    r#type: DataType::String.into(),
956                    ..Default::default()
957                })),
958                ..Default::default()
959            },
960            parse_type_no_err("string[521]", &HashMap::new())
961        );
962        assert_eq!(
963            MemberType {
964                r#type: DataType::Array.into(),
965                size: 521,
966                array_type: Some(Box::new(MemberType {
967                    r#type: DataType::Uint.into(),
968                    size: 4,
969                    ..Default::default()
970                })),
971                ..Default::default()
972            },
973            parse_type_no_err("uint32[521]", &HashMap::new())
974        );
975        assert_eq!(
976            MemberType {
977                r#type: DataType::Array.into(),
978                array_type: Some(Box::new(MemberType {
979                    r#type: DataType::Array.into(),
980                    size: 521,
981                    array_type: Some(Box::new(MemberType {
982                        r#type: DataType::Uint.into(),
983                        size: 4,
984                        ..Default::default()
985                    })),
986                    ..Default::default()
987                })),
988                ..Default::default()
989            },
990            parse_type_no_err("uint32[521][]", &HashMap::new())
991        );
992
993        // Structs
994        assert!(parse_type("Unknown", &HashMap::new()).is_err());
995
996        assert_eq!(
997            MemberType {
998                r#type: DataType::Struct.into(),
999                struct_name: "Person".to_string(),
1000                ..Default::default()
1001            },
1002            parse_type_no_err(
1003                "Person",
1004                &HashMap::from([("Person".to_string(), Vec::new())])
1005            )
1006        );
1007    }
1008
1009    #[test]
1010    fn test_encode_value() {
1011        let encoded =
1012            encode_value(&parse_type_no_err("bytes", &HashMap::new()), &"foo".into()).unwrap();
1013        assert_eq!(b"foo".to_vec(), encoded);
1014
1015        let encoded = encode_value(
1016            &parse_type_no_err("bytes3", &HashMap::new()),
1017            &"0xaabbcc".into(),
1018        )
1019        .unwrap();
1020        assert_eq!(vec![0xaa, 0xbb, 0xcc], encoded);
1021
1022        let encoded = encode_value(
1023            &parse_type_no_err("uint64", &HashMap::new()),
1024            &2983742332.0.into(),
1025        )
1026        .unwrap();
1027        assert_eq!(vec![0xb1, 0xd8, 0x4b, 0x7c], encoded);
1028
1029        let encoded = encode_value(
1030            &parse_type_no_err("uint64", &HashMap::new()),
1031            &"0xb1d84b7c".into(),
1032        )
1033        .unwrap();
1034        assert_eq!(vec![0xb1, 0xd8, 0x4b, 0x7c], encoded);
1035
1036        let encoded =
1037            encode_value(&parse_type_no_err("uint64", &HashMap::new()), &"0x1".into()).unwrap();
1038        assert_eq!(vec![0x01], encoded);
1039
1040        let encoded = encode_value(
1041            &parse_type_no_err("uint64", &HashMap::new()),
1042            &"0x0001".into(),
1043        )
1044        .unwrap();
1045        assert_eq!(vec![0x01], encoded);
1046
1047        assert!(encode_value(
1048            &parse_type_no_err("uint64", &HashMap::new()),
1049            &"0xnot correct".into(),
1050        )
1051        .is_err());
1052
1053        let encoded = encode_value(
1054            &parse_type_no_err("int64", &HashMap::new()),
1055            &2983742332.0.into(),
1056        )
1057        .unwrap();
1058        assert_eq!(vec![0xb1, 0xd8, 0x4b, 0x7c], encoded);
1059
1060        let encoded = encode_value(
1061            &parse_type_no_err("int64", &HashMap::new()),
1062            &(-2983742332.0).into(),
1063        )
1064        .unwrap();
1065        assert_eq!(vec![0xff, 0x4e, 0x27, 0xb4, 0x84], encoded);
1066
1067        let encoded =
1068            encode_value(&parse_type_no_err("string", &HashMap::new()), &"foo".into()).unwrap();
1069        assert_eq!(b"foo".to_vec(), encoded);
1070
1071        let encoded = encode_value(
1072            &parse_type_no_err("address", &HashMap::new()),
1073            &"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC".into(),
1074        )
1075        .unwrap();
1076        assert_eq!(
1077            b"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC".to_vec(),
1078            encoded
1079        );
1080
1081        let encoded =
1082            encode_value(&parse_type_no_err("bool", &HashMap::new()), &false.into()).unwrap();
1083        assert_eq!(vec![0], encoded);
1084
1085        let encoded =
1086            encode_value(&parse_type_no_err("bool", &HashMap::new()), &true.into()).unwrap();
1087        assert_eq!(vec![1], encoded);
1088
1089        // Array encodes its size.
1090        let encoded = encode_value(
1091            &parse_type_no_err("bool[]", &HashMap::new()),
1092            &Value::Array(vec![]),
1093        )
1094        .unwrap();
1095        assert_eq!(b"\x00\x00\x00\x00".to_vec(), encoded);
1096
1097        let encoded = encode_value(
1098            &parse_type_no_err("uint8[]", &HashMap::new()),
1099            &Value::Array(vec![1.into(); 10]),
1100        )
1101        .unwrap();
1102        assert_eq!(b"\x00\x00\x00\x0a".to_vec(), encoded);
1103
1104        let encoded = encode_value(
1105            &parse_type_no_err("uint8[]", &HashMap::new()),
1106            &Value::Array(vec![1.into(); 1000]),
1107        )
1108        .unwrap();
1109        assert_eq!(b"\x00\x00\x03\xe8".to_vec(), encoded);
1110    }
1111
1112    #[test]
1113    fn test_get_value() {
1114        let msg: Eip712Message = serde_json::from_str(EIP712_MSG).unwrap();
1115
1116        // DOMAIN
1117
1118        // Can't lookup domain itself.
1119        assert!(get_value(
1120            &pb::EthTypedMessageValueResponse {
1121                root_object: RootObject::Domain as _,
1122                path: vec![],
1123            },
1124            &msg,
1125        )
1126        .is_err());
1127        // Path points to nowhere.
1128        assert!(get_value(
1129            &pb::EthTypedMessageValueResponse {
1130                root_object: RootObject::Domain as _,
1131                path: vec![0, 0],
1132            },
1133            &msg,
1134        )
1135        .is_err());
1136
1137        // domain.name
1138        let value = get_value(
1139            &pb::EthTypedMessageValueResponse {
1140                root_object: RootObject::Domain as _,
1141                path: vec![0],
1142            },
1143            &msg,
1144        )
1145        .unwrap();
1146        assert_eq!(value, b"Ether Mail".to_vec());
1147
1148        // domain.version
1149        let value = get_value(
1150            &pb::EthTypedMessageValueResponse {
1151                root_object: RootObject::Domain as _,
1152                path: vec![1],
1153            },
1154            &msg,
1155        )
1156        .unwrap();
1157        assert_eq!(value, b"1".to_vec());
1158
1159        // domain.chainId
1160        let value = get_value(
1161            &pb::EthTypedMessageValueResponse {
1162                root_object: RootObject::Domain as _,
1163                path: vec![2],
1164            },
1165            &msg,
1166        )
1167        .unwrap();
1168        assert_eq!(value, b"\x01".to_vec());
1169
1170        // domain.verifyingContract
1171        let value = get_value(
1172            &pb::EthTypedMessageValueResponse {
1173                root_object: RootObject::Domain as _,
1174                path: vec![3],
1175            },
1176            &msg,
1177        )
1178        .unwrap();
1179        assert_eq!(
1180            value,
1181            b"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC".to_vec()
1182        );
1183        // No more members.
1184        assert!(get_value(
1185            &pb::EthTypedMessageValueResponse {
1186                root_object: RootObject::Domain as _,
1187                path: vec![4],
1188            },
1189            &msg,
1190        )
1191        .is_err());
1192
1193        // MESSAGE
1194
1195        // message.from.name
1196        let value = get_value(
1197            &pb::EthTypedMessageValueResponse {
1198                root_object: RootObject::Message as _,
1199                path: vec![0, 0],
1200            },
1201            &msg,
1202        )
1203        .unwrap();
1204        assert_eq!(value, b"Cow".to_vec());
1205
1206        // message.from.wallet
1207        let value = get_value(
1208            &pb::EthTypedMessageValueResponse {
1209                root_object: RootObject::Message as _,
1210                path: vec![0, 1],
1211            },
1212            &msg,
1213        )
1214        .unwrap();
1215        assert_eq!(
1216            value,
1217            b"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826".to_vec()
1218        );
1219
1220        // message.to.wallet
1221        let value = get_value(
1222            &pb::EthTypedMessageValueResponse {
1223                root_object: RootObject::Message as _,
1224                path: vec![1, 1],
1225            },
1226            &msg,
1227        )
1228        .unwrap();
1229        assert_eq!(
1230            value,
1231            b"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB".to_vec()
1232        );
1233
1234        // message.attachments.0.contents
1235        let value = get_value(
1236            &pb::EthTypedMessageValueResponse {
1237                root_object: RootObject::Message as _,
1238                path: vec![3, 0, 0],
1239            },
1240            &msg,
1241        )
1242        .unwrap();
1243        assert_eq!(value, b"attachment1".to_vec());
1244
1245        // message.attachments.1.contents
1246        let value = get_value(
1247            &pb::EthTypedMessageValueResponse {
1248                root_object: RootObject::Message as _,
1249                path: vec![3, 1, 0],
1250            },
1251            &msg,
1252        )
1253        .unwrap();
1254        assert_eq!(value, b"attachment2".to_vec());
1255
1256        // no more attachments
1257        assert!(get_value(
1258            &pb::EthTypedMessageValueResponse {
1259                root_object: RootObject::Message as _,
1260                path: vec![3, 2, 0],
1261            },
1262            &msg,
1263        )
1264        .is_err());
1265
1266        // only one field in attachment
1267        assert!(get_value(
1268            &pb::EthTypedMessageValueResponse {
1269                root_object: RootObject::Message as _,
1270                path: vec![3, 1, 1],
1271            },
1272            &msg,
1273        )
1274        .is_err());
1275    }
1276
1277    #[test]
1278    fn test_eth_identify_case() {
1279        assert_eq!(
1280            eth_identify_case("0XF39FD6E51AAD88F6F4CE6AB8827279CFFFB92266"),
1281            pb::EthAddressCase::Upper
1282        );
1283        assert_eq!(
1284            eth_identify_case("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"),
1285            pb::EthAddressCase::Lower
1286        );
1287        assert_eq!(
1288            eth_identify_case("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
1289            pb::EthAddressCase::Mixed
1290        );
1291    }
1292}