hw-wallet 0.1.2

Shared wallet orchestration utilities for CLI and FFI
Documentation
use trezor_connect::thp::{SolanaSignTx, SolanaTxAdditionalInfo, SolanaTxTokenAccountInfo};

use super::*;
use crate::hex::decode;
use crate::{WalletError, WalletErrorKind};

const SOL_TX_VERSIONS: &str = include_str!("../../../../tests/data/solana/sol_tx_versions.json");

fn fixture(key: &str) -> Vec<u8> {
    let fixtures: serde_json::Value = serde_json::from_str(SOL_TX_VERSIONS).unwrap();
    decode(fixtures[key].as_str().unwrap()).unwrap()
}

fn sol_path() -> Vec<u32> {
    vec![0x8000_002c, 0x8000_01f5, 0x8000_0000, 0x8000_0000]
}

fn with_signatures(count: u8, message: Vec<u8>) -> Vec<u8> {
    let mut tx = vec![count];
    tx.extend(std::iter::repeat_n(0u8, 64 * usize::from(count)));
    tx.extend(message);
    tx
}

fn with_version_byte(version_byte: u8) -> Vec<u8> {
    let mut tx = fixture("v0_message_without_lookup_tables");
    tx[0] = version_byte;
    tx
}

#[test]
fn from_serialized_tx_accepts_legacy_and_v0_envelopes() {
    let cases = [
        ("legacy message", fixture("legacy_message")),
        (
            "serialized legacy tx",
            with_signatures(2, fixture("legacy_message")),
        ),
        (
            "v0 message with lookup tables",
            fixture("v0_message_with_lookup_tables"),
        ),
        (
            "v0 message without lookup tables",
            fixture("v0_message_without_lookup_tables"),
        ),
    ];
    for (name, tx) in cases {
        let request = SolanaSignTx::from_serialized_tx(sol_path(), tx.clone())
            .unwrap_or_else(|err| panic!("{name} should be accepted: {err}"));
        assert_eq!(request.serialized_tx, tx, "{name}");
        assert_eq!(request.path, sol_path(), "{name}");
        assert!(request.additional_info.is_none(), "{name}");
    }
}

#[test]
fn from_serialized_tx_rejects_versioned_envelopes_other_than_v0() {
    let mut serialized_v1_tx = fixture("v1_message");
    serialized_v1_tx.extend([0u8; 64]);
    let cases = [
        ("v1 message", fixture("v1_message"), 1),
        ("serialized v1 tx", serialized_v1_tx, 1),
        ("v2 prefix", with_version_byte(0x82), 2),
        ("highest version prefix", with_version_byte(0xff), 0x7f),
    ];
    for (name, tx, version) in cases {
        let err = SolanaSignTx::from_serialized_tx(sol_path(), tx).unwrap_err();
        assert!(
            matches!(err, WalletError::UnsupportedSolanaTxVersion(actual) if actual == version),
            "{name}: {err}"
        );
        assert_eq!(err.kind(), WalletErrorKind::Validation, "{name}");
    }
}

#[test]
fn from_serialized_tx_rejects_short_payloads() {
    for tx in [Vec::new(), vec![0x81], vec![0x01, 0x02, 0x03]] {
        let len = tx.len();
        let err = SolanaSignTx::from_serialized_tx(sol_path(), tx).unwrap_err();
        assert!(matches!(
            err,
            WalletError::SolanaTxTooShort { len: actual, min: MIN_SERIALIZED_TX_BYTES } if actual == len
        ));
        assert_eq!(err.kind(), WalletErrorKind::Validation);
    }
}

#[test]
fn additional_info_input_maps_token_accounts_and_hex_definition() {
    let token_account_json = r#"{
        "base_address": "base",
        "token_program": "program",
        "token_mint": "mint",
        "token_account": "account"
    }"#;
    let token_account = SolanaTxTokenAccountInfo {
        base_address: "base".into(),
        token_program: "program".into(),
        token_mint: "mint".into(),
        token_account: "account".into(),
    };
    let cases = [
        (
            "empty object",
            "{}".to_string(),
            SolanaTxAdditionalInfo::default(),
        ),
        (
            "token account and definition",
            format!(
                r#"{{"token_accounts_infos": [{token_account_json}], "encoded_token": "0xdead"}}"#
            ),
            SolanaTxAdditionalInfo {
                token_accounts_infos: vec![token_account],
                encoded_token: Some(vec![0xde, 0xad]),
            },
        ),
    ];
    for (name, json, expected) in cases {
        let input = TxAdditionalInfoInput::from_json(&json)
            .unwrap_or_else(|err| panic!("{name} should parse: {err}"));
        assert_eq!(SolanaTxAdditionalInfo::from(input), expected, "{name}");
    }
}

#[test]
fn additional_info_input_rejects_malformed_input() {
    let cases = [
        ("not JSON", "token"),
        (
            "token account missing mint",
            r#"{"token_accounts_infos": [{"base_address": "a", "token_program": "b", "token_account": "d"}]}"#,
        ),
        ("non-hex definition", r#"{"encoded_token": "zz"}"#),
    ];
    for (name, json) in cases {
        let err = TxAdditionalInfoInput::from_json(json).unwrap_err();
        assert_eq!(err.kind(), WalletErrorKind::Validation, "{name}");
    }
}