use hw_wallet::ble::{
BootstrapTarget, SessionBootstrap, SessionBootstrapOptions, SessionPhase, SessionRetryPolicy,
};
use hw_wallet::btc::TxInput as BtcTxInput;
use hw_wallet::message::{SignMessageRequestExt, SignTypedDataRequestExt};
use trezor_connect::thp::testing::MockBackend;
use trezor_connect::thp::{
BtcSignTx, Chain, EthSignTx, EthTxSignature, HostConfig, PairingMethod, SignMessageRequest,
SignTxRequest, SignTxResponse, SignTypedDataRequest, SolanaSignTx, ThpWorkflow,
};
const BTC_SIGN_WITH_REF_TXS: &str =
include_str!("../../../tests/data/bitcoin/btc_sign_with_ref_txs.json");
const ETH_PATH: [u32; 5] = [0x8000_002c, 0x8000_003c, 0x8000_0000, 0, 0];
async fn ready_workflow() -> ThpWorkflow<MockBackend> {
let mut config = HostConfig::new("test-host", "hw-core/cli");
config.pairing_methods = vec![PairingMethod::CodeEntry];
let backend = MockBackend::paired_connection_flow().with_transient_session_failure();
let mut workflow = ThpWorkflow::new(backend, config);
let options = SessionBootstrapOptions {
try_to_unlock: true,
retry_policy: SessionRetryPolicy {
retry_delay_ms: 1,
..SessionRetryPolicy::default()
},
..SessionBootstrapOptions::default()
};
let phase = workflow
.advance_session_bootstrap(false, BootstrapTarget::Session, &options)
.await
.unwrap();
assert_eq!(phase, SessionPhase::Ready);
workflow
}
#[tokio::test]
async fn bootstrap_confirms_connection_and_retries_session() {
let mut workflow = ready_workflow().await;
let backend = workflow.backend_mut();
assert_eq!(backend.counters.credential_calls, 1);
assert_eq!(backend.counters.create_session_calls, 2);
assert_eq!(backend.channel_requests, vec![true]);
}
#[tokio::test]
async fn pair_target_stops_before_session_creation() {
let mut workflow = ThpWorkflow::new(
MockBackend::paired_connection_flow(),
HostConfig::new("test-host", "hw-core/cli"),
);
let phase = workflow
.advance_session_bootstrap(
false,
BootstrapTarget::Paired,
&SessionBootstrapOptions::default(),
)
.await
.unwrap();
assert_eq!(phase, SessionPhase::NeedsSession);
assert_eq!(workflow.backend_mut().counters.create_session_calls, 0);
}
#[tokio::test]
async fn sign_eth_tx_on_ready_session() {
let mut workflow = ready_workflow().await;
let request = EthSignTx::new(ETH_PATH.to_vec(), 1)
.with_nonce(vec![0])
.with_gas_limit(vec![0x52, 0x08])
.with_max_fee_per_gas(vec![1])
.with_max_priority_fee(vec![1])
.with_to("0x000000000000000000000000000000000000dead".into())
.with_value(vec![0]);
let response = workflow.sign_tx(request.into()).await.unwrap();
assert!(matches!(
response,
SignTxResponse::Ethereum(EthTxSignature { v: 0, .. })
));
let backend = workflow.backend_mut();
assert_eq!(backend.counters.sign_tx_calls, 1);
let Some(SignTxRequest::Ethereum(request)) = &backend.last_sign_tx_request else {
panic!("expected an Ethereum sign request");
};
assert_eq!(request.chain_id, 1);
}
#[tokio::test]
async fn sign_sol_tx_uses_solana_chain() {
let mut workflow = ready_workflow().await;
let path = vec![0x8000_002c, 0x8000_01f5, 0x8000_0000, 0x8000_0000];
let request = SolanaSignTx {
path: path.clone(),
serialized_tx: vec![0x01, 0x02, 0x03],
additional_info: None,
};
let response = workflow.sign_tx(request.into()).await.unwrap();
assert!(matches!(response, SignTxResponse::Solana { signature } if signature.len() == 64));
let Some(SignTxRequest::Solana(request)) = workflow.backend_mut().last_sign_tx_request.clone()
else {
panic!("expected a Solana sign request");
};
assert_eq!(request.path, path);
assert_eq!(request.serialized_tx, vec![0x01, 0x02, 0x03]);
}
#[tokio::test]
async fn sign_btc_tx_uses_bitcoin_chain() {
let mut workflow = ready_workflow().await;
let tx = BtcTxInput::from_json(BTC_SIGN_WITH_REF_TXS).unwrap();
let response = workflow
.sign_tx(BtcSignTx::try_from(tx).unwrap().into())
.await
.unwrap();
assert!(
matches!(response, SignTxResponse::Bitcoin { last_signature, .. } if last_signature.len() == 64)
);
assert!(matches!(
workflow.backend_mut().last_sign_tx_request,
Some(SignTxRequest::Bitcoin(_))
));
}
#[tokio::test]
async fn sign_message_uses_requested_chain() {
let cases = [
(Chain::Ethereum, ETH_PATH.to_vec(), "hello", false, true),
(
Chain::Bitcoin,
vec![0x8000_002c, 0x8000_0000, 0x8000_0000],
"68656c6c6f",
true,
false,
),
];
for (chain, path, message, is_hex, chunkify) in cases {
let mut workflow = ready_workflow().await;
let request =
SignMessageRequest::from_message(chain, path, message, is_hex, chunkify, &[]).unwrap();
let response = workflow.sign_message(request).await.unwrap();
assert_eq!(response.chain, chain);
assert_eq!(response.signature.len(), 65);
let backend = workflow.backend_mut();
assert_eq!(backend.counters.sign_message_calls, 1);
let request = backend.last_sign_message_request.as_ref().unwrap();
assert_eq!(request.chain, chain);
assert_eq!(request.chunkify, chunkify);
}
}
#[tokio::test]
async fn sign_typed_data_uses_ethereum_chain() {
let mut workflow = ready_workflow().await;
let request = SignTypedDataRequest::from_eip712_json(
ETH_PATH.to_vec(),
r#"{
"types": {
"EIP712Domain": [{ "name": "name", "type": "string" }],
"Mail": [
{ "name": "from", "type": "address" },
{ "name": "contents", "type": "string" }
]
},
"primaryType": "Mail",
"domain": { "name": "Ether Mail" },
"message": { "from": "0x1111111111111111111111111111111111111111", "contents": "hello" }
}"#,
true,
)
.unwrap();
let response = workflow.sign_typed_data(request).await.unwrap();
assert_eq!(response.chain, Chain::Ethereum);
assert_eq!(response.signature.len(), 65);
let backend = workflow.backend_mut();
assert_eq!(backend.counters.sign_typed_data_calls, 1);
let request = backend.last_sign_typed_data_request.as_ref().unwrap();
assert_eq!(request.chain, Chain::Ethereum);
}