use std::collections::HashMap;
use std::marker::PhantomData;
use std::str::FromStr;
use crate::{
vsptrait::{
Transaction, VisualSignConverter, VisualSignConverterFromString, VisualSignError,
VisualSignOptions,
},
SignablePayload,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Chain {
Unspecified,
Solana,
Ethereum,
Bitcoin,
Sui,
Aptos,
Polkadot,
Tron,
Custom(String), }
impl Chain {
pub fn as_str(&self) -> &str {
match self {
Chain::Unspecified => "Unspecified",
Chain::Solana => "Solana",
Chain::Ethereum => "Ethereum",
Chain::Bitcoin => "Bitcoin",
Chain::Sui => "Sui",
Chain::Aptos => "Aptos",
Chain::Polkadot => "Polkadot",
Chain::Tron => "Tron",
Chain::Custom(name) => name.as_str(),
}
}
}
impl FromStr for Chain {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.to_lowercase().as_str() {
"unspecified" => Chain::Unspecified,
"solana" => Chain::Solana,
"ethereum" => Chain::Ethereum,
"bitcoin" => Chain::Bitcoin,
"sui" => Chain::Sui,
"aptos" => Chain::Aptos,
"polkadot" => Chain::Polkadot,
"tron" => Chain::Tron,
_ => Chain::Custom(s.to_string()),
})
}
}
pub trait VisualSignConverterAny: Send + Sync {
fn to_visual_sign_payload_from_string_any(
&self,
transaction_data: &str,
options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError>;
fn supports_format(&self, transaction_data: &str) -> bool;
}
struct ConverterWrapper<T, C>
where
T: Transaction + Send + Sync,
C: VisualSignConverter<T> + VisualSignConverterFromString<T>,
{
converter: C,
_phantom: PhantomData<T>,
}
impl<T, C> ConverterWrapper<T, C>
where
T: Transaction + Send + Sync,
C: VisualSignConverter<T> + VisualSignConverterFromString<T>,
{
fn new(converter: C) -> Self {
Self {
converter,
_phantom: PhantomData,
}
}
}
impl<T, C> VisualSignConverterAny for ConverterWrapper<T, C>
where
T: Transaction + Send + Sync, C: VisualSignConverter<T> + VisualSignConverterFromString<T> + Send + Sync,
{
fn to_visual_sign_payload_from_string_any(
&self,
transaction_data: &str,
options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError> {
self.converter
.to_visual_sign_payload_from_string(transaction_data, options)
}
fn supports_format(&self, transaction_data: &str) -> bool {
T::from_string(transaction_data).is_ok()
}
}
pub struct TransactionConverterRegistry {
converters: HashMap<Chain, Box<dyn VisualSignConverterAny>>,
}
impl Default for TransactionConverterRegistry {
fn default() -> Self {
Self::new()
}
}
impl TransactionConverterRegistry {
pub fn new() -> Self {
Self {
converters: HashMap::new(),
}
}
pub fn register<T, C>(&mut self, chain: Chain, converter: C)
where
T: Transaction + Send + Sync + 'static,
C: VisualSignConverter<T> + VisualSignConverterFromString<T> + Send + Sync + 'static,
{
self.converters
.insert(chain, Box::new(ConverterWrapper::<T, C>::new(converter)));
}
pub fn get_converter(&self, chain: &Chain) -> Option<&dyn VisualSignConverterAny> {
self.converters.get(chain).map(|c| c.as_ref())
}
pub fn convert_transaction(
&self,
chain: &Chain,
transaction_data: &str,
options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError> {
match self.get_converter(chain) {
Some(converter) => {
converter.to_visual_sign_payload_from_string_any(transaction_data, options)
}
None => Err(VisualSignError::ConversionError(format!(
"No converter registered for chain: {}",
chain.as_str()
))),
}
}
pub fn auto_detect_and_convert(
&self,
transaction_data: &str,
options: VisualSignOptions,
) -> Result<(Chain, SignablePayload), VisualSignError> {
for (chain, converter) in &self.converters {
if converter.supports_format(transaction_data) {
match converter
.to_visual_sign_payload_from_string_any(transaction_data, options.clone())
{
Ok(payload) => return Ok((chain.clone(), payload)),
Err(_) => continue, }
}
}
Err(VisualSignError::ConversionError(
"Could not detect transaction type or no compatible converter found".to_string(),
))
}
pub fn supported_chains(&self) -> Vec<Chain> {
self.converters.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{SignablePayloadField, SignablePayloadFieldCommon, SignablePayloadFieldTextV2};
use crate::vsptrait::TransactionParseError;
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct MockSolanaTransaction {
data: Vec<u8>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct MockEthereumTransaction {
data: Vec<u8>,
}
impl Transaction for MockSolanaTransaction {
fn from_string(data: &str) -> Result<Self, TransactionParseError> {
if data.starts_with("01") {
let bytes = match decode_hex(data) {
Ok(b) => b,
Err(_) => {
return Err(TransactionParseError::DecodeError(
"Invalid hex".to_string(),
))
}
};
if !bytes.is_empty() && bytes[0] == 0x01 {
return Ok(Self { data: bytes });
}
}
Err(TransactionParseError::InvalidFormat(
"Not a Solana transaction".to_string(),
))
}
fn transaction_type(&self) -> String {
"Solana".to_string()
}
}
impl Transaction for MockEthereumTransaction {
fn from_string(data: &str) -> Result<Self, TransactionParseError> {
if data.starts_with("02") {
let bytes = match decode_hex(data) {
Ok(b) => b,
Err(_) => {
return Err(TransactionParseError::DecodeError(
"Invalid hex".to_string(),
))
}
};
if !bytes.is_empty() && bytes[0] == 0x02 {
return Ok(Self { data: bytes });
}
}
Err(TransactionParseError::InvalidFormat(
"Not an Ethereum transaction".to_string(),
))
}
fn transaction_type(&self) -> String {
"Ethereum".to_string()
}
}
fn decode_hex(s: &str) -> Result<Vec<u8>, &'static str> {
if s.len() % 2 != 0 {
return Err("Hex string must have even length");
}
let mut result = Vec::with_capacity(s.len() / 2);
let mut chars = s.chars();
while let (Some(a), Some(b)) = (chars.next(), chars.next()) {
let byte = match (a.to_digit(16), b.to_digit(16)) {
(Some(high), Some(low)) => ((high as u8) << 4) | (low as u8),
_ => return Err("Invalid hex character"),
};
result.push(byte);
}
Ok(result)
}
struct MockSuccessConverter<T> {
phantom: PhantomData<T>,
}
impl<T> MockSuccessConverter<T> {
fn new() -> Self {
Self {
phantom: PhantomData,
}
}
}
impl<T: Transaction> VisualSignConverter<T> for MockSuccessConverter<T> {
fn to_visual_sign_payload(
&self,
_transaction: T,
_options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError> {
Ok(SignablePayload::new(
0,
"Test Transaction".to_string(),
None,
vec![SignablePayloadField::TextV2 {
common: SignablePayloadFieldCommon {
fallback_text: "Test".to_string(),
label: "Test Label".to_string(),
},
text_v2: SignablePayloadFieldTextV2 {
text: "Test Value".to_string(),
},
}],
"Test Source".to_string(),
))
}
}
impl<T: Transaction> VisualSignConverterFromString<T> for MockSuccessConverter<T> {}
struct MockFailingConverter<T> {
phantom: PhantomData<T>,
}
impl<T> MockFailingConverter<T> {
fn new() -> Self {
Self {
phantom: PhantomData,
}
}
}
impl<T: Transaction> VisualSignConverter<T> for MockFailingConverter<T> {
fn to_visual_sign_payload(
&self,
_transaction: T,
_options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError> {
Err(VisualSignError::ConversionError(
"Mock conversion failed".to_string(),
))
}
}
impl<T: Transaction> VisualSignConverterFromString<T> for MockFailingConverter<T> {}
#[test]
fn test_auto_detect_solana_success() {
let mut registry = TransactionConverterRegistry::new();
registry.register::<MockSolanaTransaction, _>(Chain::Solana, MockSuccessConverter::new());
registry
.register::<MockEthereumTransaction, _>(Chain::Ethereum, MockSuccessConverter::new());
let result =
registry.auto_detect_and_convert("01abcdef1234567890", VisualSignOptions::default());
assert!(result.is_ok());
let (chain, _) = result.unwrap();
assert_eq!(chain, Chain::Solana);
}
#[test]
fn test_auto_detect_ethereum_success() {
let mut registry = TransactionConverterRegistry::new();
registry.register::<MockSolanaTransaction, _>(Chain::Solana, MockSuccessConverter::new());
registry
.register::<MockEthereumTransaction, _>(Chain::Ethereum, MockSuccessConverter::new());
let result =
registry.auto_detect_and_convert("02abcdef1234567890", VisualSignOptions::default());
assert!(result.is_ok());
let (chain, _) = result.unwrap();
assert_eq!(chain, Chain::Ethereum);
}
#[test]
fn test_auto_detect_no_matching_converter() {
let mut registry = TransactionConverterRegistry::new();
registry.register::<MockSolanaTransaction, _>(Chain::Solana, MockSuccessConverter::new());
registry
.register::<MockEthereumTransaction, _>(Chain::Ethereum, MockSuccessConverter::new());
let result = registry.auto_detect_and_convert(
"03abcdef1234567890", VisualSignOptions::default(),
);
assert!(result.is_err());
}
#[test]
fn test_auto_detect_format_supported_but_conversion_fails() {
let mut registry = TransactionConverterRegistry::new();
registry.register::<MockSolanaTransaction, _>(Chain::Solana, MockFailingConverter::new());
registry
.register::<MockEthereumTransaction, _>(Chain::Ethereum, MockSuccessConverter::new());
let result = registry.auto_detect_and_convert(
"01abcdef1234567890", VisualSignOptions::default(),
);
assert!(result.is_err());
}
#[test]
fn test_empty_registry() {
let registry = TransactionConverterRegistry::new();
let result =
registry.auto_detect_and_convert("01abcdef1234567890", VisualSignOptions::default());
assert!(result.is_err());
}
#[test]
fn test_chain_from_str() {
assert_eq!(Chain::from_str("solana"), Ok(Chain::Solana));
assert_eq!(Chain::from_str("SOLANA"), Ok(Chain::Solana)); assert_eq!(Chain::from_str("ethereum"), Ok(Chain::Ethereum));
assert_eq!(Chain::from_str("bitcoin"), Ok(Chain::Bitcoin));
assert_eq!(Chain::from_str("sui"), Ok(Chain::Sui));
assert_eq!(Chain::from_str("aptos"), Ok(Chain::Aptos));
assert_eq!(Chain::from_str("polkadot"), Ok(Chain::Polkadot));
assert_eq!(Chain::from_str("tron"), Ok(Chain::Tron));
assert_eq!(
Chain::from_str("unknown"),
Ok(Chain::Custom("unknown".to_string()))
);
}
#[test]
fn test_chain_as_str() {
assert_eq!(Chain::Solana.as_str(), "Solana");
assert_eq!(Chain::Ethereum.as_str(), "Ethereum");
assert_eq!(Chain::Bitcoin.as_str(), "Bitcoin");
assert_eq!(Chain::Sui.as_str(), "Sui");
assert_eq!(Chain::Aptos.as_str(), "Aptos");
assert_eq!(Chain::Polkadot.as_str(), "Polkadot");
assert_eq!(Chain::Tron.as_str(), "Tron");
assert_eq!(Chain::Custom("MyChain".to_string()).as_str(), "MyChain");
}
}