This commit is contained in:
@@ -12,6 +12,7 @@ pub mod shortcut;
|
||||
pub mod terminal_model;
|
||||
pub mod theme;
|
||||
pub mod transfer;
|
||||
pub mod wallet_mvp;
|
||||
pub mod workspace;
|
||||
|
||||
pub use config::{AppConfig, ShortcutBinding};
|
||||
@@ -26,6 +27,12 @@ pub use secret::{SecretKind, SecretRef, SecretString};
|
||||
pub use shortcut::{validate_unique_bindings, CommandBinding, KeyChord, ShortcutSequence};
|
||||
pub use theme::Theme;
|
||||
pub use transfer::{TransferDirection, TransferEvent, TransferProtocol, TransferRequest};
|
||||
pub use wallet_mvp::{
|
||||
all_wallet_mvp_features, demo_dashboard, wallet_feature_summary, AccountSource, ActivityEntry,
|
||||
ApprovalRecord, ChainConfig, Contact, DappPermission, SendRequest, SigningRequest, SwapQuote,
|
||||
WalletAccount, WalletDashboard, WalletFeatureArea, WalletFeatureSpec, WalletFeatureStatus,
|
||||
WalletSettings,
|
||||
};
|
||||
pub use workspace::{ConnectionKind, PaneNode, Profile, SplitAxis, Tab, Workspace};
|
||||
|
||||
impl Workspace {
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum WalletFeatureStatus {
|
||||
Modeled,
|
||||
UiPrototype,
|
||||
RuntimeImplemented,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum WalletFeatureArea {
|
||||
Onboarding,
|
||||
Accounts,
|
||||
Portfolio,
|
||||
DappConnection,
|
||||
Signing,
|
||||
Transaction,
|
||||
DeFi,
|
||||
Security,
|
||||
Customization,
|
||||
Data,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct WalletFeatureSpec {
|
||||
pub key: &'static str,
|
||||
pub title: &'static str,
|
||||
pub area: WalletFeatureArea,
|
||||
pub rabby_reference: &'static str,
|
||||
pub mvp_acceptance: &'static str,
|
||||
pub status: WalletFeatureStatus,
|
||||
}
|
||||
|
||||
pub const RABBY_WALLET_MVP_FEATURES: &[WalletFeatureSpec] = &[
|
||||
WalletFeatureSpec { key: "onboarding-password", title: "Create/unlock wallet", area: WalletFeatureArea::Onboarding, rabby_reference: "Welcome, CreatePassword, Unlock, ForgotPassword", mvp_acceptance: "User can create a password, lock/unlock, and keep encrypted local state", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "import-create-accounts", title: "Import/create accounts", area: WalletFeatureArea::Accounts, rabby_reference: "CreateMnemonics, ImportMnemonics, ImportPrivateKey, ImportJson, watch address", mvp_acceptance: "Seed phrase/private key/JSON/watch-only account records validate and display", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "hardware-wallets", title: "Hardware and institutional accounts", area: WalletFeatureArea::Accounts, rabby_reference: "Ledger, Trezor, Keystone, GridPlus, OneKey, BitBox02, Gnosis Safe, Coinbase, WalletConnect, Cobo Argus", mvp_acceptance: "Account source model covers hardware/safe/walletconnect/custody sources", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "multi-chain", title: "Multi-chain network support", area: WalletFeatureArea::Portfolio, rabby_reference: "ChainList, CustomRPC, CustomTestnet, offline chain support", mvp_acceptance: "Chains have IDs, RPC URLs, testnet/custom/offline flags, and validation", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "portfolio-dashboard", title: "Portfolio dashboard", area: WalletFeatureArea::Portfolio, rabby_reference: "Dashboard, token/NFT/DeFi DB services, balance sync", mvp_acceptance: "Dashboard summary shows accounts, chains, token balances, NFTs, DeFi positions", status: WalletFeatureStatus::UiPrototype },
|
||||
WalletFeatureSpec { key: "send-receive", title: "Send and receive", area: WalletFeatureArea::Transaction, rabby_reference: "SendToken, SendNFT, Receive, SelectToAddress", mvp_acceptance: "Send request validates recipient, amount, asset, chain, and receive view exposes address", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "swap-bridge", title: "Swap and bridge", area: WalletFeatureArea::DeFi, rabby_reference: "Swap, Bridge, rabby-swap, rabby-bridge", mvp_acceptance: "Quote model validates from/to asset, chain route, slippage, and provider", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "approvals", title: "Token and NFT approvals", area: WalletFeatureArea::Security, rabby_reference: "ManageApprovals, NFTApproval, TokenApproval, ManageBatchApprovals", mvp_acceptance: "Approval records identify spender, asset, allowance, risk level, revoke action", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "dapp-provider", title: "Dapp provider and permissions", area: WalletFeatureArea::DappConnection, rabby_reference: "content-script, pageProvider, providerController, RequestPermission, ConnectedSites", mvp_acceptance: "Dapp connection request validates origin, requested accounts/chains, and permission status", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "signing-security", title: "Signing and security previews", area: WalletFeatureArea::Signing, rabby_reference: "Approval, securityEngine, rabby-action, transaction docs, sign message highlighter", mvp_acceptance: "Signing request includes typed data/message/transaction preview, simulation result, and risk checks", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "history-activity", title: "Activity and transaction history", area: WalletFeatureArea::Data, rabby_reference: "Activities, History, TransactionHistory, SignedTextHistory", mvp_acceptance: "History entries record kind, account, chain, status, timestamp, and tx hash or signature id", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "contacts-whitelist", title: "Contacts and whitelist", area: WalletFeatureArea::Security, rabby_reference: "contactBook, WhitelistInput, SelectToAddress", mvp_acceptance: "Contact and whitelist records validate address/name and can be matched before send", status: WalletFeatureStatus::Modeled },
|
||||
WalletFeatureSpec { key: "settings-customization", title: "Settings and customization", area: WalletFeatureArea::Customization, rabby_reference: "AdvanceSettings, SwitchLang, currency, autoLock, preference, metamask mode", mvp_acceptance: "Settings model covers theme, language, currency, autolock, default wallet/metamask mode", status: WalletFeatureStatus::UiPrototype },
|
||||
WalletFeatureSpec { key: "notifications-points", title: "Notifications, guides, points", area: WalletFeatureArea::Data, rabby_reference: "notification, newUserGuide, RabbyPoints, feedback", mvp_acceptance: "Notification and points summary can be listed in the dashboard shell", status: WalletFeatureStatus::UiPrototype },
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AccountSource {
|
||||
SeedPhrase,
|
||||
PrivateKey,
|
||||
JsonKeystore,
|
||||
WatchOnly,
|
||||
Ledger,
|
||||
Trezor,
|
||||
Keystone,
|
||||
GridPlus,
|
||||
OneKey,
|
||||
BitBox02,
|
||||
GnosisSafe,
|
||||
Coinbase,
|
||||
WalletConnect,
|
||||
CoboArgus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WalletAccount {
|
||||
pub id: String,
|
||||
pub address: String,
|
||||
pub name: String,
|
||||
pub source: AccountSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ChainConfig {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub rpc_url: String,
|
||||
pub is_testnet: bool,
|
||||
pub is_custom: bool,
|
||||
pub is_offline_supported: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TokenBalance {
|
||||
pub chain_id: u64,
|
||||
pub symbol: String,
|
||||
pub amount: f64,
|
||||
pub usd_value: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RiskLevel {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Critical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ApprovalRecord {
|
||||
pub account_id: String,
|
||||
pub chain_id: u64,
|
||||
pub asset_symbol: String,
|
||||
pub spender: String,
|
||||
pub allowance: String,
|
||||
pub risk: RiskLevel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SendRequest {
|
||||
pub from_account_id: String,
|
||||
pub to_address: String,
|
||||
pub chain_id: u64,
|
||||
pub asset_symbol: String,
|
||||
pub amount: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SwapQuote {
|
||||
pub provider: String,
|
||||
pub from_chain_id: u64,
|
||||
pub to_chain_id: u64,
|
||||
pub from_symbol: String,
|
||||
pub to_symbol: String,
|
||||
pub from_amount: f64,
|
||||
pub estimated_to_amount: f64,
|
||||
pub slippage_bps: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PermissionStatus {
|
||||
Pending,
|
||||
Approved,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DappPermission {
|
||||
pub origin: String,
|
||||
pub accounts: Vec<String>,
|
||||
pub chains: Vec<u64>,
|
||||
pub status: PermissionStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SigningKind {
|
||||
Message,
|
||||
TypedData,
|
||||
Transaction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SigningRequest {
|
||||
pub origin: String,
|
||||
pub account_id: String,
|
||||
pub kind: SigningKind,
|
||||
pub preview: String,
|
||||
pub risk: RiskLevel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ActivityStatus {
|
||||
Pending,
|
||||
Confirmed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ActivityEntry {
|
||||
pub id: String,
|
||||
pub account_id: String,
|
||||
pub chain_id: u64,
|
||||
pub label: String,
|
||||
pub status: ActivityStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Contact {
|
||||
pub name: String,
|
||||
pub address: String,
|
||||
pub whitelisted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ThemeMode {
|
||||
Light,
|
||||
Dark,
|
||||
System,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WalletSettings {
|
||||
pub theme: ThemeMode,
|
||||
pub language: String,
|
||||
pub currency: String,
|
||||
pub auto_lock_minutes: u16,
|
||||
pub prefer_rabby_over_metamask: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WalletDashboard {
|
||||
pub active_account: WalletAccount,
|
||||
pub chains: Vec<ChainConfig>,
|
||||
pub balances: Vec<TokenBalance>,
|
||||
pub approvals: Vec<ApprovalRecord>,
|
||||
pub activities: Vec<ActivityEntry>,
|
||||
pub settings: WalletSettings,
|
||||
}
|
||||
|
||||
impl WalletAccount {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
validate_id(&self.id, "account id")?;
|
||||
validate_address(&self.address)?;
|
||||
if self.name.trim().is_empty() {
|
||||
return Err("account name is required".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainConfig {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.id == 0 {
|
||||
return Err("chain id is required".to_string());
|
||||
}
|
||||
if self.name.trim().is_empty() {
|
||||
return Err("chain name is required".to_string());
|
||||
}
|
||||
if !(self.rpc_url.starts_with("https://")
|
||||
|| self.rpc_url.starts_with("http://")
|
||||
|| self.rpc_url == "offline")
|
||||
{
|
||||
return Err("chain RPC URL must be http(s) or offline".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SendRequest {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
validate_id(&self.from_account_id, "from account")?;
|
||||
validate_address(&self.to_address)?;
|
||||
if self.chain_id == 0 {
|
||||
return Err("chain id is required".to_string());
|
||||
}
|
||||
if self.asset_symbol.trim().is_empty() {
|
||||
return Err("asset symbol is required".to_string());
|
||||
}
|
||||
if self.amount <= 0.0 {
|
||||
return Err("amount must be positive".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SwapQuote {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.provider.trim().is_empty() {
|
||||
return Err("swap provider is required".to_string());
|
||||
}
|
||||
if self.from_chain_id == 0 || self.to_chain_id == 0 {
|
||||
return Err("swap chain ids are required".to_string());
|
||||
}
|
||||
if self.from_symbol.trim().is_empty() || self.to_symbol.trim().is_empty() {
|
||||
return Err("swap assets are required".to_string());
|
||||
}
|
||||
if self.from_amount <= 0.0 || self.estimated_to_amount <= 0.0 {
|
||||
return Err("swap amounts must be positive".to_string());
|
||||
}
|
||||
if self.slippage_bps > 5_000 {
|
||||
return Err("slippage is too high".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DappPermission {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if !(self.origin.starts_with("https://") || self.origin.starts_with("http://")) {
|
||||
return Err("dapp origin must be http(s)".to_string());
|
||||
}
|
||||
if self.accounts.is_empty() {
|
||||
return Err("dapp permission requires at least one account".to_string());
|
||||
}
|
||||
if self.chains.is_empty() {
|
||||
return Err("dapp permission requires at least one chain".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletSettings {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.language.trim().is_empty() {
|
||||
return Err("language is required".to_string());
|
||||
}
|
||||
if self.currency.trim().is_empty() {
|
||||
return Err("currency is required".to_string());
|
||||
}
|
||||
if self.auto_lock_minutes == 0 {
|
||||
return Err("auto lock must be at least one minute".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletDashboard {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
self.active_account.validate()?;
|
||||
for chain in &self.chains {
|
||||
chain.validate()?;
|
||||
}
|
||||
self.settings.validate()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn total_usd_value(&self) -> f64 {
|
||||
self.balances.iter().map(|token| token.usd_value).sum()
|
||||
}
|
||||
|
||||
pub fn high_risk_approval_count(&self) -> usize {
|
||||
self.approvals
|
||||
.iter()
|
||||
.filter(|approval| matches!(approval.risk, RiskLevel::High | RiskLevel::Critical))
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_wallet_mvp_features() -> &'static [WalletFeatureSpec] {
|
||||
RABBY_WALLET_MVP_FEATURES
|
||||
}
|
||||
|
||||
pub fn missing_wallet_mvp_features<'a>(required_keys: &'a [&'a str]) -> Vec<&'a str> {
|
||||
required_keys
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|key| {
|
||||
!RABBY_WALLET_MVP_FEATURES
|
||||
.iter()
|
||||
.any(|feature| feature.key == *key)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn wallet_feature_summary() -> Vec<(&'static str, &'static str, WalletFeatureStatus)> {
|
||||
RABBY_WALLET_MVP_FEATURES
|
||||
.iter()
|
||||
.map(|feature| (feature.title, feature.mvp_acceptance, feature.status))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn demo_dashboard() -> WalletDashboard {
|
||||
WalletDashboard {
|
||||
active_account: WalletAccount {
|
||||
id: "main".to_string(),
|
||||
address: "0x1111111111111111111111111111111111111111".to_string(),
|
||||
name: "Main Wallet".to_string(),
|
||||
source: AccountSource::SeedPhrase,
|
||||
},
|
||||
chains: vec![
|
||||
ChainConfig {
|
||||
id: 1,
|
||||
name: "Ethereum".to_string(),
|
||||
rpc_url: "https://rpc.ankr.com/eth".to_string(),
|
||||
is_testnet: false,
|
||||
is_custom: false,
|
||||
is_offline_supported: true,
|
||||
},
|
||||
ChainConfig {
|
||||
id: 8453,
|
||||
name: "Base".to_string(),
|
||||
rpc_url: "https://mainnet.base.org".to_string(),
|
||||
is_testnet: false,
|
||||
is_custom: false,
|
||||
is_offline_supported: true,
|
||||
},
|
||||
],
|
||||
balances: vec![
|
||||
TokenBalance {
|
||||
chain_id: 1,
|
||||
symbol: "ETH".to_string(),
|
||||
amount: 1.24,
|
||||
usd_value: 4200.00,
|
||||
},
|
||||
TokenBalance {
|
||||
chain_id: 8453,
|
||||
symbol: "USDC".to_string(),
|
||||
amount: 1280.0,
|
||||
usd_value: 1280.0,
|
||||
},
|
||||
],
|
||||
approvals: vec![ApprovalRecord {
|
||||
account_id: "main".to_string(),
|
||||
chain_id: 1,
|
||||
asset_symbol: "USDC".to_string(),
|
||||
spender: "0x2222222222222222222222222222222222222222".to_string(),
|
||||
allowance: "unlimited".to_string(),
|
||||
risk: RiskLevel::High,
|
||||
}],
|
||||
activities: vec![ActivityEntry {
|
||||
id: "act-1".to_string(),
|
||||
account_id: "main".to_string(),
|
||||
chain_id: 1,
|
||||
label: "Swap ETH → USDC".to_string(),
|
||||
status: ActivityStatus::Confirmed,
|
||||
}],
|
||||
settings: WalletSettings {
|
||||
theme: ThemeMode::System,
|
||||
language: "en".to_string(),
|
||||
currency: "USD".to_string(),
|
||||
auto_lock_minutes: 15,
|
||||
prefer_rabby_over_metamask: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_id(value: &str, label: &str) -> Result<(), String> {
|
||||
if value.trim().is_empty() {
|
||||
return Err(format!("{label} is required"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_address(address: &str) -> Result<(), String> {
|
||||
if address.len() != 42
|
||||
|| !address.starts_with("0x")
|
||||
|| !address.chars().skip(2).all(|ch| ch.is_ascii_hexdigit())
|
||||
{
|
||||
return Err("address must be a 20-byte hex Ethereum address".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
const REQUIRED_RABBY_MVP: &[&str] = &[
|
||||
"onboarding-password",
|
||||
"import-create-accounts",
|
||||
"hardware-wallets",
|
||||
"multi-chain",
|
||||
"portfolio-dashboard",
|
||||
"send-receive",
|
||||
"swap-bridge",
|
||||
"approvals",
|
||||
"dapp-provider",
|
||||
"signing-security",
|
||||
"history-activity",
|
||||
"contacts-whitelist",
|
||||
"settings-customization",
|
||||
"notifications-points",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn feature_inventory_covers_rabbyhub_mvp_classes() {
|
||||
assert_eq!(
|
||||
missing_wallet_mvp_features(REQUIRED_RABBY_MVP),
|
||||
Vec::<&str>::new()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_inventory_keys_are_unique_and_described() {
|
||||
let mut keys = HashSet::new();
|
||||
for feature in all_wallet_mvp_features() {
|
||||
assert!(
|
||||
keys.insert(feature.key),
|
||||
"duplicate feature key {}",
|
||||
feature.key
|
||||
);
|
||||
assert!(!feature.title.trim().is_empty());
|
||||
assert!(!feature.rabby_reference.trim().is_empty());
|
||||
assert!(!feature.mvp_acceptance.trim().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_sources_cover_rabby_import_surface() {
|
||||
let sources = [
|
||||
AccountSource::SeedPhrase,
|
||||
AccountSource::PrivateKey,
|
||||
AccountSource::JsonKeystore,
|
||||
AccountSource::WatchOnly,
|
||||
AccountSource::Ledger,
|
||||
AccountSource::Trezor,
|
||||
AccountSource::Keystone,
|
||||
AccountSource::GridPlus,
|
||||
AccountSource::OneKey,
|
||||
AccountSource::BitBox02,
|
||||
AccountSource::GnosisSafe,
|
||||
AccountSource::Coinbase,
|
||||
AccountSource::WalletConnect,
|
||||
AccountSource::CoboArgus,
|
||||
];
|
||||
assert_eq!(sources.len(), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_account_chain_send_swap_and_dapp_requests() {
|
||||
let account = WalletAccount {
|
||||
id: "a1".to_string(),
|
||||
address: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
|
||||
name: "Vault".to_string(),
|
||||
source: AccountSource::Ledger,
|
||||
};
|
||||
assert!(account.validate().is_ok());
|
||||
|
||||
let chain = ChainConfig {
|
||||
id: 1,
|
||||
name: "Ethereum".to_string(),
|
||||
rpc_url: "https://rpc.example".to_string(),
|
||||
is_testnet: false,
|
||||
is_custom: false,
|
||||
is_offline_supported: true,
|
||||
};
|
||||
assert!(chain.validate().is_ok());
|
||||
|
||||
let send = SendRequest {
|
||||
from_account_id: "a1".to_string(),
|
||||
to_address: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
|
||||
chain_id: 1,
|
||||
asset_symbol: "ETH".to_string(),
|
||||
amount: 0.5,
|
||||
};
|
||||
assert!(send.validate().is_ok());
|
||||
|
||||
let quote = SwapQuote {
|
||||
provider: "Rabby Swap".to_string(),
|
||||
from_chain_id: 1,
|
||||
to_chain_id: 8453,
|
||||
from_symbol: "ETH".to_string(),
|
||||
to_symbol: "USDC".to_string(),
|
||||
from_amount: 1.0,
|
||||
estimated_to_amount: 3300.0,
|
||||
slippage_bps: 50,
|
||||
};
|
||||
assert!(quote.validate().is_ok());
|
||||
|
||||
let permission = DappPermission {
|
||||
origin: "https://debank.com".to_string(),
|
||||
accounts: vec!["a1".to_string()],
|
||||
chains: vec![1, 8453],
|
||||
status: PermissionStatus::Pending,
|
||||
};
|
||||
assert!(permission.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsafe_or_incomplete_wallet_requests() {
|
||||
let bad_send = SendRequest {
|
||||
from_account_id: "".to_string(),
|
||||
to_address: "not-an-address".to_string(),
|
||||
chain_id: 0,
|
||||
asset_symbol: "".to_string(),
|
||||
amount: 0.0,
|
||||
};
|
||||
assert!(bad_send.validate().is_err());
|
||||
|
||||
let bad_quote = SwapQuote {
|
||||
provider: "".to_string(),
|
||||
from_chain_id: 1,
|
||||
to_chain_id: 1,
|
||||
from_symbol: "ETH".to_string(),
|
||||
to_symbol: "USDC".to_string(),
|
||||
from_amount: 1.0,
|
||||
estimated_to_amount: 1.0,
|
||||
slippage_bps: 9000,
|
||||
};
|
||||
assert!(bad_quote.validate().is_err());
|
||||
|
||||
let bad_permission = DappPermission {
|
||||
origin: "javascript:alert(1)".to_string(),
|
||||
accounts: vec![],
|
||||
chains: vec![],
|
||||
status: PermissionStatus::Pending,
|
||||
};
|
||||
assert!(bad_permission.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashboard_summarizes_value_and_risk() {
|
||||
let dashboard = demo_dashboard();
|
||||
|
||||
assert!(dashboard.validate().is_ok());
|
||||
assert_eq!(dashboard.total_usd_value(), 5480.0);
|
||||
assert_eq!(dashboard.high_risk_approval_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_require_customization_defaults() {
|
||||
let settings = WalletSettings {
|
||||
theme: ThemeMode::Dark,
|
||||
language: "en".to_string(),
|
||||
currency: "USD".to_string(),
|
||||
auto_lock_minutes: 5,
|
||||
prefer_rabby_over_metamask: true,
|
||||
};
|
||||
assert!(settings.validate().is_ok());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user