use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BipPath {
pub indices: Vec<u32>,
}
impl BipPath {
pub fn new(indices: Vec<u32>) -> Result<Self, String> {
if indices.len() > crate::instructions::length::MAX_BIP32_PATH_DEPTH {
return Err(format!(
"BIP32 path too deep: {} (max {})",
indices.len(),
crate::instructions::length::MAX_BIP32_PATH_DEPTH
));
}
Ok(BipPath { indices })
}
pub fn from_string(path_str: &str) -> Result<Self, String> {
const PADDING: u32 = 0x80000000;
if !path_str.starts_with("m/") {
return Err("BIP32 path must start with 'm/'".to_string());
}
let components: Vec<&str> = path_str[2..].split('/').collect();
let mut indices = Vec::new();
for component in components {
if component.is_empty() {
continue;
}
let (number_str, is_hardened) = if let Some(stripped) = component.strip_suffix("'") {
(stripped, true)
} else {
(component, false)
};
let number: u32 = number_str
.parse()
.map_err(|_| format!("Invalid number in path component: {}", component))?;
let final_number = if is_hardened {
number + PADDING
} else {
number
};
indices.push(final_number);
}
if indices.is_empty() {
return Err("BIP32 path cannot be empty".to_string());
}
Self::new(indices)
}
pub fn ethereum_standard(account: u32, address_index: u32) -> Self {
BipPath {
indices: vec![
0x8000002C, 0x8000003C, 0x80000000 | account, 0, address_index, ],
}
}
pub fn encoded_len(&self) -> usize {
1 + self.indices.len() * crate::instructions::length::BIP32_INDEX_SIZE
}
}
impl fmt::Display for BipPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "m")?;
for index in &self.indices {
if *index >= 0x80000000 {
write!(f, "/{}'", index - 0x80000000)?;
} else {
write!(f, "/{}", index)?;
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EthAddress {
pub address: String,
}
impl EthAddress {
pub fn new(address: String) -> Result<Self, String> {
if !address.starts_with("0x") {
return Err("Ethereum address must start with 0x".to_string());
}
if address.len() != 42 {
return Err("Ethereum address must be 42 characters long".to_string());
}
Ok(EthAddress { address })
}
pub fn without_prefix(&self) -> &str {
&self.address[2..]
}
pub fn to_bytes(&self) -> Result<Vec<u8>, hex::FromHexError> {
hex::decode(self.without_prefix())
}
}
impl fmt::Display for EthAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.address)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicKeyInfo {
pub public_key: Vec<u8>,
pub address: EthAddress,
pub chain_code: Option<Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signature {
pub v: u8,
pub r: Vec<u8>,
pub s: Vec<u8>,
}
impl Signature {
pub fn new(v: u8, r: Vec<u8>, s: Vec<u8>) -> Result<Self, String> {
if r.len() != crate::instructions::length::SIGNATURE_COMPONENT_SIZE {
return Err(format!("Invalid r length: {} (expected 32)", r.len()));
}
if s.len() != crate::instructions::length::SIGNATURE_COMPONENT_SIZE {
return Err(format!("Invalid s length: {} (expected 32)", s.len()));
}
Ok(Signature { v, r, s })
}
pub fn to_der(&self) -> Vec<u8> {
let mut result = Vec::new();
result.push(self.v);
result.extend_from_slice(&self.r);
result.extend_from_slice(&self.s);
result
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppConfiguration {
pub flags: ConfigFlags,
pub version: AppVersion,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConfigFlags {
pub arbitrary_data_signature: bool,
pub erc20_external_info: bool,
pub transaction_check_enabled: bool,
pub transaction_check_opt_in: bool,
}
impl ConfigFlags {
pub fn from_byte(flags: u8) -> Self {
ConfigFlags {
arbitrary_data_signature: (flags
& crate::instructions::config_flags::ARBITRARY_DATA_SIGNATURE)
!= 0,
erc20_external_info: (flags & crate::instructions::config_flags::ERC20_EXTERNAL_INFO)
!= 0,
transaction_check_enabled: (flags
& crate::instructions::config_flags::TRANSACTION_CHECK_ENABLED)
!= 0,
transaction_check_opt_in: (flags
& crate::instructions::config_flags::TRANSACTION_CHECK_OPT_IN)
!= 0,
}
}
pub fn to_byte(&self) -> u8 {
let mut flags = 0u8;
if self.arbitrary_data_signature {
flags |= crate::instructions::config_flags::ARBITRARY_DATA_SIGNATURE;
}
if self.erc20_external_info {
flags |= crate::instructions::config_flags::ERC20_EXTERNAL_INFO;
}
if self.transaction_check_enabled {
flags |= crate::instructions::config_flags::TRANSACTION_CHECK_ENABLED;
}
if self.transaction_check_opt_in {
flags |= crate::instructions::config_flags::TRANSACTION_CHECK_OPT_IN;
}
flags
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppVersion {
pub major: u8,
pub minor: u8,
pub patch: u8,
}
impl fmt::Display for AppVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl AppVersion {
pub fn new(major: u8, minor: u8, patch: u8) -> Self {
Self {
major,
minor,
patch,
}
}
pub fn supports_eip712_v0(&self) -> bool {
self.major > 1 || (self.major == 1 && self.minor >= 5)
}
pub fn supports_eip712_full(&self) -> bool {
self.major > 1
|| (self.major == 1 && self.minor > 9)
|| (self.major == 1 && self.minor == 9 && self.patch >= 19)
}
pub fn compare(&self, other: &AppVersion) -> std::cmp::Ordering {
use std::cmp::Ordering;
match self.major.cmp(&other.major) {
Ordering::Equal => match self.minor.cmp(&other.minor) {
Ordering::Equal => self.patch.cmp(&other.patch),
other => other,
},
other => other,
}
}
pub fn is_at_least(&self, other: &AppVersion) -> bool {
matches!(
self.compare(other),
std::cmp::Ordering::Greater | std::cmp::Ordering::Equal
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GetAddressParams {
pub path: BipPath,
pub display: bool,
pub return_chain_code: bool,
pub chain_id: Option<u64>,
}
impl GetAddressParams {
pub fn new(path: BipPath) -> Self {
GetAddressParams {
path,
display: false,
return_chain_code: false,
chain_id: None,
}
}
pub fn with_display(mut self) -> Self {
self.display = true;
self
}
pub fn with_chain_code(mut self) -> Self {
self.return_chain_code = true;
self
}
pub fn with_chain_id(mut self, chain_id: u64) -> Self {
self.chain_id = Some(chain_id);
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignTransactionParams {
pub path: BipPath,
pub transaction_data: Vec<u8>,
}
impl SignTransactionParams {
pub fn new(path: BipPath, transaction_data: Vec<u8>) -> Self {
SignTransactionParams {
path,
transaction_data,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignMessageParams {
pub path: BipPath,
pub message: Vec<u8>,
}
impl SignMessageParams {
pub fn new(path: BipPath, message: Vec<u8>) -> Self {
SignMessageParams { path, message }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Eip712Mode {
V0Implementation,
FullImplementation,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignEip712Params {
pub path: BipPath,
pub domain_hash: [u8; 32],
pub message_hash: [u8; 32],
}
impl SignEip712Params {
pub fn new(path: BipPath, domain_hash: [u8; 32], message_hash: [u8; 32]) -> Self {
SignEip712Params {
path,
domain_hash,
message_hash,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Eip712FieldType {
Custom(String),
Int(u8),
Uint(u8),
Address,
Bool,
String,
FixedBytes(u8),
DynamicBytes,
}
impl Eip712FieldType {
pub fn type_id(&self) -> u8 {
match self {
Eip712FieldType::Custom(_) => 0,
Eip712FieldType::Int(_) => 1,
Eip712FieldType::Uint(_) => 2,
Eip712FieldType::Address => 3,
Eip712FieldType::Bool => 4,
Eip712FieldType::String => 5,
Eip712FieldType::FixedBytes(_) => 6,
Eip712FieldType::DynamicBytes => 7,
}
}
pub fn type_size(&self) -> Option<u8> {
match self {
Eip712FieldType::Int(size) => Some(*size),
Eip712FieldType::Uint(size) => Some(*size),
Eip712FieldType::FixedBytes(size) => Some(*size),
_ => None,
}
}
pub fn type_name(&self) -> Option<&str> {
match self {
Eip712FieldType::Custom(name) => Some(name),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Eip712ArrayLevel {
Dynamic,
Fixed(u8),
}
impl Eip712ArrayLevel {
pub fn type_id(&self) -> u8 {
match self {
Eip712ArrayLevel::Dynamic => 0,
Eip712ArrayLevel::Fixed(_) => 1,
}
}
pub fn size(&self) -> Option<u8> {
match self {
Eip712ArrayLevel::Fixed(size) => Some(*size),
Eip712ArrayLevel::Dynamic => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Eip712FieldDefinition {
pub field_type: Eip712FieldType,
pub name: String,
pub array_levels: Vec<Eip712ArrayLevel>,
}
impl Eip712FieldDefinition {
pub fn new(field_type: Eip712FieldType, name: String) -> Self {
Eip712FieldDefinition {
field_type,
name,
array_levels: Vec::new(),
}
}
pub fn with_array_level(mut self, level: Eip712ArrayLevel) -> Self {
self.array_levels.push(level);
self
}
pub fn is_array(&self) -> bool {
!self.array_levels.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Eip712StructDefinition {
pub name: String,
pub fields: Vec<Eip712FieldDefinition>,
}
impl Eip712StructDefinition {
pub fn new(name: String) -> Self {
Eip712StructDefinition {
name,
fields: Vec::new(),
}
}
pub fn with_field(mut self, field: Eip712FieldDefinition) -> Self {
self.fields.push(field);
self
}
pub fn with_sorted_fields(mut self) -> Self {
self.fields.sort_by(|a, b| a.name.cmp(&b.name));
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Eip712FieldValue {
pub value: Vec<u8>,
}
impl Eip712FieldValue {
pub fn new(value: Vec<u8>) -> Self {
Eip712FieldValue { value }
}
pub fn from_string(s: &str) -> Self {
Eip712FieldValue {
value: s.as_bytes().to_vec(),
}
}
pub fn from_u256(value: &[u8; 32]) -> Self {
Eip712FieldValue {
value: value.to_vec(),
}
}
pub fn from_address(address: &[u8; 20]) -> Self {
Eip712FieldValue {
value: address.to_vec(),
}
}
pub fn from_bool(value: bool) -> Self {
Eip712FieldValue {
value: vec![if value { 1 } else { 0 }],
}
}
pub fn from_uint(value: u64) -> Self {
Eip712FieldValue {
value: value.to_be_bytes().to_vec(),
}
}
pub fn from_uint_sized(size: u8, value: u64) -> Self {
let mut bytes = vec![0u8; size as usize];
let value_bytes = value.to_be_bytes();
let start = bytes.len().saturating_sub(value_bytes.len());
let copy_len = (bytes.len() - start).min(value_bytes.len());
bytes[start..start + copy_len]
.copy_from_slice(&value_bytes[value_bytes.len() - copy_len..]);
Eip712FieldValue { value: bytes }
}
pub fn from_uint32(value: u32) -> Self {
Eip712FieldValue {
value: value.to_be_bytes().to_vec(),
}
}
pub fn from_address_string(address: &str) -> Result<Self, String> {
let hex_str = if let Some(stripped) = address.strip_prefix("0x") {
stripped
} else {
address
};
if hex_str.len() != 40 {
return Err(format!(
"Invalid address length: expected 40 hex characters, got {}",
hex_str.len()
));
}
let bytes = hex::decode(hex_str).map_err(|e| format!("Invalid hex: {}", e))?;
if bytes.len() != 20 {
return Err("Address must be 20 bytes".to_string());
}
Ok(Eip712FieldValue { value: bytes })
}
pub fn from_struct() -> Self {
Eip712FieldValue { value: vec![] }
}
pub fn from_int_sized(size: u8, value: i64) -> Self {
let mut bytes = vec![0u8; size as usize];
let value_bytes = value.to_be_bytes();
let start = bytes.len().saturating_sub(value_bytes.len());
let copy_len = (bytes.len() - start).min(value_bytes.len());
bytes[start..start + copy_len]
.copy_from_slice(&value_bytes[value_bytes.len() - copy_len..]);
Eip712FieldValue { value: bytes }
}
pub fn from_bytes(bytes: Vec<u8>) -> Self {
Eip712FieldValue { value: bytes }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Eip712StructImplementation {
pub name: String,
pub values: Vec<Eip712FieldValue>,
}
impl Eip712StructImplementation {
pub fn new(name: String) -> Self {
Eip712StructImplementation {
name,
values: Vec::new(),
}
}
pub fn with_value(mut self, value: Eip712FieldValue) -> Self {
self.values.push(value);
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Eip712FilterType {
Activation,
DiscardedFilterPath(String),
MessageInfo {
display_name: String,
filters_count: u8,
signature: Vec<u8>,
},
TrustedName {
display_name: String,
name_types: Vec<u8>,
name_sources: Vec<u8>,
signature: Vec<u8>,
},
DateTime {
display_name: String,
signature: Vec<u8>,
},
AmountJoinToken { token_index: u8, signature: Vec<u8> },
AmountJoinValue {
display_name: String,
token_index: u8,
signature: Vec<u8>,
},
RawField {
display_name: String,
signature: Vec<u8>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Eip712FilterParams {
pub filter_type: Eip712FilterType,
pub discarded: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Eip712Domain {
pub name: Option<String>,
pub version: Option<String>,
pub chain_id: Option<u64>,
pub verifying_contract: Option<String>,
pub salt: Option<Vec<u8>>,
}
impl Eip712Domain {
pub fn new() -> Self {
Eip712Domain {
name: None,
version: None,
chain_id: None,
verifying_contract: None,
salt: None,
}
}
pub fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
pub fn with_version(mut self, version: String) -> Self {
self.version = Some(version);
self
}
pub fn with_chain_id(mut self, chain_id: u64) -> Self {
self.chain_id = Some(chain_id);
self
}
pub fn with_verifying_contract(mut self, address: String) -> Self {
self.verifying_contract = Some(address);
self
}
pub fn with_salt(mut self, salt: Vec<u8>) -> Self {
self.salt = Some(salt);
self
}
}
impl Default for Eip712Domain {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Eip712Field {
pub name: String,
pub r#type: String,
}
impl Eip712Field {
pub fn new(name: String, r#type: String) -> Self {
Eip712Field { name, r#type }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Eip712Struct {
pub fields: Vec<Eip712Field>,
}
impl Eip712Struct {
pub fn new() -> Self {
Eip712Struct { fields: Vec::new() }
}
pub fn with_field(mut self, field: Eip712Field) -> Self {
self.fields.push(field);
self
}
}
impl Default for Eip712Struct {
fn default() -> Self {
Self::new()
}
}
pub type Eip712Types = HashMap<String, Eip712Struct>;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Eip712TypedData {
pub domain: Eip712Domain,
pub types: Eip712Types,
pub primary_type: String,
pub message: serde_json::Value,
}
impl Eip712TypedData {
pub fn new(
domain: Eip712Domain,
types: Eip712Types,
primary_type: String,
message: serde_json::Value,
) -> Self {
Eip712TypedData {
domain,
types,
primary_type,
message,
}
}
}
#[cfg(test)]
mod eip712_typed_data_tests {
use super::*;
#[test]
fn test_eip712_domain_creation() {
let domain = Eip712Domain::new()
.with_name("Ether Mail".to_string())
.with_version("1".to_string())
.with_chain_id(1)
.with_verifying_contract("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC".to_string());
assert_eq!(domain.name, Some("Ether Mail".to_string()));
assert_eq!(domain.version, Some("1".to_string()));
assert_eq!(domain.chain_id, Some(1));
assert_eq!(
domain.verifying_contract,
Some("0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC".to_string())
);
}
#[test]
fn test_eip712_struct_creation() {
let person_struct = Eip712Struct::new()
.with_field(Eip712Field::new("name".to_string(), "string".to_string()))
.with_field(Eip712Field::new(
"wallet".to_string(),
"address".to_string(),
));
assert_eq!(person_struct.fields.len(), 2);
assert_eq!(person_struct.fields[0].name, "name");
assert_eq!(person_struct.fields[0].r#type, "string");
assert_eq!(person_struct.fields[1].name, "wallet");
assert_eq!(person_struct.fields[1].r#type, "address");
}
#[test]
fn test_eip712_typed_data_creation() {
let domain = Eip712Domain::new()
.with_name("Ether Mail".to_string())
.with_version("1".to_string())
.with_chain_id(1);
let mut types = Eip712Types::new();
types.insert(
"Person".to_string(),
Eip712Struct::new()
.with_field(Eip712Field::new("name".to_string(), "string".to_string()))
.with_field(Eip712Field::new(
"wallet".to_string(),
"address".to_string(),
)),
);
let message = serde_json::json!({
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
});
let typed_data = Eip712TypedData::new(domain, types, "Mail".to_string(), message);
assert_eq!(typed_data.primary_type, "Mail");
assert!(typed_data.types.contains_key("Person"));
}
}