diff --git a/apps/desktop-wallet/src-tauri/src/commands.rs b/apps/desktop-wallet/src-tauri/src/commands.rs index 335b7ea..c7a855c 100644 --- a/apps/desktop-wallet/src-tauri/src/commands.rs +++ b/apps/desktop-wallet/src-tauri/src/commands.rs @@ -774,3 +774,456 @@ pub async fn wallet_get_fee_estimate( ) -> Result { app_state.rpc_client.get_fee_estimate().await } + +// ============================================================================ +// Smart Contract Commands +// ============================================================================ + +/// Contract deployment request +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeployContractRequest { + /// Contract bytecode (hex) + pub bytecode: String, + /// Constructor arguments (encoded) + pub constructor_args: Option, + /// Gas limit + pub gas_limit: u64, + /// Initial value to send (in sompi) + pub value: u64, +} + +/// Contract deployment response +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeployContractResponse { + /// Transaction ID + pub tx_id: String, + /// Contract address (available after confirmation) + pub contract_address: Option, + /// Gas used + pub gas_used: u64, +} + +/// Deploy a smart contract +#[tauri::command] +pub async fn contract_deploy( + wallet_state: State<'_, WalletState>, + app_state: State<'_, AppState>, + request: DeployContractRequest, +) -> Result { + if !wallet_state.is_unlocked().await { + return Err(Error::WalletLocked); + } + + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + // TODO: Build and sign contract deployment transaction + // 1. Create transaction with contract bytecode in payload + // 2. Sign with wallet key + // 3. Broadcast to network + + // For now, return a placeholder + Ok(DeployContractResponse { + tx_id: "pending".to_string(), + contract_address: None, + gas_used: 0, + }) +} + +/// Contract call request +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CallContractRequest { + /// Contract address + pub contract_address: String, + /// Method to call (encoded) + pub method: String, + /// Arguments (encoded) + pub args: Option, + /// Gas limit + pub gas_limit: u64, + /// Value to send (in sompi) + pub value: u64, +} + +/// Contract call response +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CallContractResponse { + /// Transaction ID (for state-changing calls) + pub tx_id: Option, + /// Return data (for view calls) + pub result: Option, + /// Gas used + pub gas_used: u64, +} + +/// Call a smart contract method +#[tauri::command] +pub async fn contract_call( + wallet_state: State<'_, WalletState>, + app_state: State<'_, AppState>, + request: CallContractRequest, +) -> Result { + if !wallet_state.is_unlocked().await { + return Err(Error::WalletLocked); + } + + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + // TODO: Build and execute contract call + Ok(CallContractResponse { + tx_id: None, + result: None, + gas_used: 0, + }) +} + +/// Contract read request (view function, no transaction needed) +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadContractRequest { + /// Contract address + pub contract_address: String, + /// Method to call (encoded) + pub method: String, + /// Arguments (encoded) + pub args: Option, +} + +/// Read from a smart contract (view function) +#[tauri::command] +pub async fn contract_read( + app_state: State<'_, AppState>, + request: ReadContractRequest, +) -> Result { + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + // TODO: Execute view call via RPC + Ok("0x".to_string()) +} + +/// Contract info +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ContractInfo { + /// Contract address + pub address: String, + /// Contract name (if known) + pub name: Option, + /// Contract type (e.g., "ERC20", "Custom") + pub contract_type: String, + /// Deployment transaction ID + pub deploy_tx_id: String, + /// Creation timestamp + pub created_at: i64, +} + +/// Get contract information +#[tauri::command] +pub async fn contract_get_info( + app_state: State<'_, AppState>, + address: String, +) -> Result { + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + // TODO: Query contract info from node + Ok(ContractInfo { + address, + name: None, + contract_type: "Unknown".to_string(), + deploy_tx_id: "".to_string(), + created_at: 0, + }) +} + +// ============================================================================ +// Token Commands +// ============================================================================ + +/// Token creation request +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateTokenRequest { + /// Token name + pub name: String, + /// Token symbol (e.g., "SYN") + pub symbol: String, + /// Decimal places (usually 8 or 18) + pub decimals: u8, + /// Initial supply (in smallest units) + pub initial_supply: String, + /// Maximum supply (optional, for capped tokens) + pub max_supply: Option, + /// Is mintable (can create more tokens later) + pub mintable: bool, + /// Is burnable (can destroy tokens) + pub burnable: bool, + /// Is pausable (can pause transfers) + pub pausable: bool, +} + +/// Token creation response +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateTokenResponse { + /// Deployment transaction ID + pub tx_id: String, + /// Token contract address (available after confirmation) + pub token_address: Option, + /// Token ID (unique identifier) + pub token_id: String, +} + +/// Create a new token +#[tauri::command] +pub async fn token_create( + wallet_state: State<'_, WalletState>, + app_state: State<'_, AppState>, + request: CreateTokenRequest, +) -> Result { + if !wallet_state.is_unlocked().await { + return Err(Error::WalletLocked); + } + + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + // Validate token parameters + if request.name.is_empty() || request.name.len() > 64 { + return Err(Error::Validation("Token name must be 1-64 characters".to_string())); + } + if request.symbol.is_empty() || request.symbol.len() > 8 { + return Err(Error::Validation("Token symbol must be 1-8 characters".to_string())); + } + if request.decimals > 18 { + return Err(Error::Validation("Decimals must be 0-18".to_string())); + } + + // TODO: Deploy standard token contract + // 1. Use pre-compiled token contract bytecode + // 2. Encode constructor args (name, symbol, decimals, supply, etc.) + // 3. Deploy contract + // 4. Return token address + + let token_id = format!( + "{}:{}", + request.symbol.to_uppercase(), + hex::encode([0u8; 4]) // Placeholder + ); + + Ok(CreateTokenResponse { + tx_id: "pending".to_string(), + token_address: None, + token_id, + }) +} + +/// Token information +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenInfo { + /// Token contract address + pub address: String, + /// Token name + pub name: String, + /// Token symbol + pub symbol: String, + /// Decimal places + pub decimals: u8, + /// Total supply + pub total_supply: String, + /// Maximum supply (if capped) + pub max_supply: Option, + /// Your balance + pub balance: String, + /// Is verified/trusted + pub is_verified: bool, + /// Logo URL (if available) + pub logo_url: Option, +} + +/// Get token information +#[tauri::command] +pub async fn token_get_info( + wallet_state: State<'_, WalletState>, + app_state: State<'_, AppState>, + token_address: String, +) -> Result { + let _wallet_state = wallet_state; // Silence unused warning + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + // TODO: Query token contract for info + // Call name(), symbol(), decimals(), totalSupply(), balanceOf(user) + + Ok(TokenInfo { + address: token_address, + name: "Unknown Token".to_string(), + symbol: "???".to_string(), + decimals: 8, + total_supply: "0".to_string(), + max_supply: None, + balance: "0".to_string(), + is_verified: false, + logo_url: None, + }) +} + +/// Token transfer request +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransferTokenRequest { + /// Token contract address + pub token_address: String, + /// Recipient address + pub to: String, + /// Amount to transfer (in smallest units) + pub amount: String, +} + +/// Transfer tokens +#[tauri::command] +pub async fn token_transfer( + wallet_state: State<'_, WalletState>, + app_state: State<'_, AppState>, + request: TransferTokenRequest, +) -> Result { + if !wallet_state.is_unlocked().await { + return Err(Error::WalletLocked); + } + + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + let _request = request; // Silence unused warning + + // TODO: Call token contract's transfer(to, amount) function + // 1. Encode transfer function call + // 2. Build and sign transaction + // 3. Broadcast + + Ok("pending".to_string()) +} + +/// Get token balance for an address +#[tauri::command] +pub async fn token_get_balance( + app_state: State<'_, AppState>, + token_address: String, + owner_address: String, +) -> Result { + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + let (_token_address, _owner_address) = (token_address, owner_address); // Silence unused + + // TODO: Call token contract's balanceOf(owner) function + Ok("0".to_string()) +} + +/// List tokens held by wallet +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenBalance { + /// Token address + pub address: String, + /// Token name + pub name: String, + /// Token symbol + pub symbol: String, + /// Decimals + pub decimals: u8, + /// Balance in smallest units + pub balance: String, + /// Balance formatted (e.g., "1,000.00") + pub balance_formatted: String, + /// USD value (if available) + pub usd_value: Option, +} + +/// Get all token balances for the wallet +#[tauri::command] +pub async fn token_list_balances( + wallet_state: State<'_, WalletState>, + app_state: State<'_, AppState>, +) -> Result> { + let _wallet_state = wallet_state; // Silence unused warning + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + // TODO: Query indexed token transfers to find held tokens + // Then query each token's balance + + Ok(vec![]) +} + +/// Mint tokens (if authorized) +#[tauri::command] +pub async fn token_mint( + wallet_state: State<'_, WalletState>, + app_state: State<'_, AppState>, + token_address: String, + to: String, + amount: String, +) -> Result { + if !wallet_state.is_unlocked().await { + return Err(Error::WalletLocked); + } + + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + let (_token_address, _to, _amount) = (token_address, to, amount); // Silence unused + + // TODO: Call token contract's mint(to, amount) function + Ok("pending".to_string()) +} + +/// Burn tokens +#[tauri::command] +pub async fn token_burn( + wallet_state: State<'_, WalletState>, + app_state: State<'_, AppState>, + token_address: String, + amount: String, +) -> Result { + if !wallet_state.is_unlocked().await { + return Err(Error::WalletLocked); + } + + let mode = app_state.node_manager.connection_mode().await; + if matches!(mode, ConnectionMode::Disconnected) { + return Err(Error::NotConnected); + } + + let (_token_address, _amount) = (token_address, amount); // Silence unused + + // TODO: Call token contract's burn(amount) function + Ok("pending".to_string()) +} diff --git a/apps/desktop-wallet/src-tauri/src/error.rs b/apps/desktop-wallet/src-tauri/src/error.rs index 5220ea2..9f5855f 100644 --- a/apps/desktop-wallet/src-tauri/src/error.rs +++ b/apps/desktop-wallet/src-tauri/src/error.rs @@ -62,6 +62,12 @@ pub enum Error { #[error("Mining error: {0}")] MiningError(String), + #[error("Validation error: {0}")] + Validation(String), + + #[error("Contract error: {0}")] + ContractError(String), + #[error("Internal error: {0}")] Internal(String), } diff --git a/apps/desktop-wallet/src-tauri/src/lib.rs b/apps/desktop-wallet/src-tauri/src/lib.rs index 48c52db..bc198d6 100644 --- a/apps/desktop-wallet/src-tauri/src/lib.rs +++ b/apps/desktop-wallet/src-tauri/src/lib.rs @@ -213,6 +213,19 @@ pub fn run() { commands::wallet_get_utxos, commands::wallet_get_network_info, commands::wallet_get_fee_estimate, + // Smart contracts + commands::contract_deploy, + commands::contract_call, + commands::contract_read, + commands::contract_get_info, + // Tokens + commands::token_create, + commands::token_get_info, + commands::token_transfer, + commands::token_get_balance, + commands::token_list_balances, + commands::token_mint, + commands::token_burn, // Updates check_update, install_update, diff --git a/apps/desktop-wallet/src/App.tsx b/apps/desktop-wallet/src/App.tsx index a1f9282..a4161d1 100644 --- a/apps/desktop-wallet/src/App.tsx +++ b/apps/desktop-wallet/src/App.tsx @@ -23,6 +23,8 @@ import History from './pages/History'; import Settings from './pages/Settings'; import NodeDashboard from './pages/Node/NodeDashboard'; import MiningDashboard from './pages/Mining/MiningDashboard'; +import ContractsDashboard from './pages/Contracts/ContractsDashboard'; +import TokensDashboard from './pages/Tokens/TokensDashboard'; function App() { const { isInitialized, isUnlocked } = useWalletStore(); @@ -100,6 +102,18 @@ function App() { isUnlocked ? : } /> + : + } + /> + : + } + /> state.status); + + // View state + const [activeTab, setActiveTab] = useState<'deploy' | 'interact'>('deploy'); + const [selectedContract, setSelectedContract] = useState(null); + + // Deploy form state + const [deployForm, setDeployForm] = useState({ + bytecode: '', + constructorArgs: '', + gasLimit: '1000000', + value: '0', + name: '', + abi: '', + }); + + // Interaction form state + const [interactForm, setInteractForm] = useState({ + method: '', + args: '', + gasLimit: '100000', + value: '0', + isRead: false, + }); + + // Results + const [interactResult, setInteractResult] = useState(null); + const [expandedContracts, setExpandedContracts] = useState>(new Set()); + + // Clear error on tab change + useEffect(() => { + clearError(); + setInteractResult(null); + }, [activeTab, selectedContract, clearError]); + + const handleDeploy = async () => { + if (!deployForm.bytecode) return; + + try { + const result = await deployContract( + { + bytecode: deployForm.bytecode, + constructorArgs: deployForm.constructorArgs || undefined, + gasLimit: parseInt(deployForm.gasLimit), + value: parseInt(deployForm.value), + }, + deployForm.name || undefined, + deployForm.abi || undefined + ); + + // Clear form and switch to interact tab + setDeployForm({ + bytecode: '', + constructorArgs: '', + gasLimit: '1000000', + value: '0', + name: '', + abi: '', + }); + setSelectedContract(result.contractAddress); + setActiveTab('interact'); + } catch { + // Error is handled by store + } + }; + + const handleInteract = async () => { + if (!selectedContract || !interactForm.method) return; + + setInteractResult(null); + + try { + if (interactForm.isRead) { + const result = await readContract( + selectedContract, + interactForm.method, + interactForm.args || undefined + ); + setInteractResult(result); + } else { + const result = await callContract({ + contractAddress: selectedContract, + method: interactForm.method, + args: interactForm.args || undefined, + gasLimit: parseInt(interactForm.gasLimit), + value: parseInt(interactForm.value), + }); + setInteractResult(`TX: ${result.txHash}\nGas Used: ${result.gasUsed}${result.result ? `\nResult: ${result.result}` : ''}`); + } + } catch { + // Error is handled by store + } + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + + const toggleContractExpanded = (address: string) => { + const newExpanded = new Set(expandedContracts); + if (newExpanded.has(address)) { + newExpanded.delete(address); + } else { + newExpanded.add(address); + } + setExpandedContracts(newExpanded); + }; + + return ( +
+ {/* Header */} +
+
+

+ + Smart Contracts +

+

+ Deploy and interact with smart contracts on Synor +

+
+
+ + {/* Not connected warning */} + {!nodeStatus.isConnected && ( +
+ Please connect to a node to deploy and interact with contracts +
+ )} + + {/* Error display */} + {error && ( +
+ {error} + +
+ )} + + {/* Main content grid */} +
+ {/* Left: Deployed Contracts List */} +
+
+

+ + Deployed Contracts +

+ + {deployedContracts.length === 0 ? ( +

No contracts deployed yet

+ ) : ( +
+ {deployedContracts.map((contract) => ( +
+
toggleContractExpanded(contract.address)} + > +
+ {expandedContracts.has(contract.address) ? ( + + ) : ( + + )} +
+

+ {contract.name} +

+

+ {truncateAddress(contract.address)} +

+
+
+
+ + +
+
+ + {expandedContracts.has(contract.address) && ( +
+
+ Deployed: + + {new Date(contract.deployedAt).toLocaleDateString()} + +
+
+ TX: + + {truncateAddress(contract.txHash, 6)} + +
+ +
+ )} +
+ ))} +
+ )} +
+
+ + {/* Right: Deploy / Interact Tabs */} +
+
+ {/* Tabs */} +
+ + +
+ + {/* Deploy Tab */} + {activeTab === 'deploy' && ( +
+
+ + + setDeployForm({ ...deployForm, name: e.target.value }) + } + placeholder="My Contract" + className="w-full px-4 py-3 rounded-lg bg-gray-800 border border-gray-700 text-white placeholder-gray-500 focus:outline-none focus:border-synor-500" + /> +
+ +
+ +