hw-wallet 0.1.2

Shared wallet orchestration utilities for CLI and FFI
Documentation
use k256::ecdsa::SigningKey;
use trezor_connect::thp::{
    EthDefinitionCatalog, EthDefinitions, EthDisplayFormatDefinition, EthSignTx,
    EthTokenDefinition, EthTxSignature,
};

use crate::WalletError;

use super::*;

const ETH_PARSE_MINIMAL: &str =
    include_str!("../../../../tests/data/ethereum/eth_parse_minimal.json");
const ETH_BUILD_SIGN_REQUEST: &str =
    include_str!("../../../../tests/data/ethereum/eth_build_sign_request.json");
const ETH_SIGN_WITH_DEFINITIONS: &str =
    include_str!("../../../../tests/data/ethereum/eth_sign_with_definitions.json");

#[test]
fn parse_tx_json_minimal() {
    let tx = TxInput::from_json(ETH_PARSE_MINIMAL).unwrap();
    assert_eq!(tx.to, "0xdead");
    assert_eq!(tx.chain_id, 1);
}

#[test]
fn omitted_tx_data_is_empty_calldata() {
    let tx = TxInput::from_json(ETH_PARSE_MINIMAL).unwrap();
    let request = tx.into_sign_tx(vec![0x8000_002c]).unwrap();
    assert!(request.data.is_empty());
}

#[test]
fn into_sign_tx_decodes_json_quantities() {
    let tx = TxInput::from_json(ETH_BUILD_SIGN_REQUEST).unwrap();

    let path = vec![0x8000_002c, 0x8000_003c, 0x8000_0000, 0, 0];
    let request = tx.into_sign_tx(path.clone()).unwrap();
    assert_eq!(request.path, path);
    assert_eq!(request.chain_id, 1);
    assert_eq!(request.nonce, vec![1]);
    assert_eq!(request.gas_limit, vec![0x52, 0x08]);
}

#[test]
fn into_sign_tx_maps_definitions_and_catalog() {
    let tx = TxInput::from_json(ETH_SIGN_WITH_DEFINITIONS).unwrap();
    let request = tx.into_sign_tx(Vec::new()).unwrap();

    assert_eq!(
        request.definitions,
        EthDefinitions {
            encoded_network: Some(b"trzd0".to_vec()),
            encoded_token: Some(vec![1, 2]),
            encoded_display_format: None,
        }
    );
    let usdt = ::hex::decode("dac17f958d2ee523a2206206994597c13d831ec7").unwrap();
    assert_eq!(
        request.definition_catalog,
        EthDefinitionCatalog {
            tokens: vec![EthTokenDefinition {
                address: [0xAA; 20],
                encoded: vec![3, 4],
            }],
            display_formats: vec![EthDisplayFormatDefinition {
                address: usdt.try_into().unwrap(),
                func_sig: [0xa9, 0x05, 0x9c, 0xbb],
                encoded: vec![5, 6],
            }],
        }
    );
}

#[test]
fn into_sign_tx_rejects_malformed_definitions() {
    for definitions in [
        r#"{"tokens": [{"address": "0xaa", "encoded": "0x01"}]}"#,
        r#"{"display_formats": [{"address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "func_sig": "0x01", "encoded": "0x01"}]}"#,
    ] {
        let json = format!(r#"{{"to": "0xdead", "definitions": {definitions}}}"#);
        let tx = TxInput::from_json(&json).unwrap();
        assert!(matches!(
            tx.into_sign_tx(Vec::new()),
            Err(WalletError::Signing(_))
        ));
    }
    let json = r#"{"to": "0xdead", "definitions": {"encoded_network": "0xzz"}}"#;
    assert!(matches!(
        TxInput::from_json(json),
        Err(WalletError::Signing(_))
    ));
}

#[test]
fn verify_sign_tx_response_recovers_expected_address() {
    let request = EthSignTx::new(vec![0x8000_002c, 0x8000_003c, 0x8000_0000, 0, 0], 1)
        .with_nonce(vec![0x01])
        .with_max_fee_per_gas(vec![0x3b, 0x9a, 0xca, 0x00])
        .with_max_priority_fee(vec![0x59, 0x68, 0x2f, 0x00])
        .with_gas_limit(vec![0x52, 0x08])
        .with_to("0x000000000000000000000000000000000000dead".into())
        .with_value(vec![0x00])
        .with_data(Vec::new());

    let tx_hash = request.eip1559_sighash().unwrap();
    let key_bytes =
        hex::decode("4c0883a69102937d6231471b5dbb6204fe512961708279ef4f8f6f1842f5f6d4").unwrap();
    let signing_key = SigningKey::from_slice(&key_bytes).unwrap();
    let (signature, recovery_id) = signing_key.sign_prehash_recoverable(&tx_hash);
    let response = EthTxSignature {
        v: u32::from(recovery_id.to_byte()),
        r: signature.r().to_bytes().to_vec(),
        s: signature.s().to_bytes().to_vec(),
    };

    let verified = VerifiedSignature::recover(&request, &response).unwrap();
    assert_eq!(
        verified.recovered_address,
        "0x5637C997D8aFf61a4EC7d606f461c1c75f2b8120"
    );
}