Skip to main content

hanzo_mining/
evm.rs

1//! EVM Integration for AI Coin Mining Rewards
2//!
3//! AI Coin is the native currency mined on the open AI protocol (BitTorrent-style).
4//! Mining rewards are earned by:
5//! - Sharing training data
6//! - Providing compute (GPU/CPU)
7//! - Keeping AI models loaded (model hosting)
8//! - Hosting specific registered models/embeddings
9//!
10//! Mined AI coins can be "teleported" via the Teleport Protocol to:
11//! - Lux C-Chain (primary L1)
12//! - Zoo EVM (chain ID 200200)
13//! - Hanzo EVM (chain ID 36963)
14
15use crate::{NetworkType, PerformanceStats};
16use serde::{Deserialize, Serialize};
17use std::sync::Arc;
18use tokio::sync::RwLock;
19
20/// AI Mining Contract addresses per network
21pub mod contracts {
22    /// Hanzo EVM Mining Contract
23    pub const HANZO_MAINNET_MINING: &str = "0x369000000000000000000000000000000000aAAI";
24    /// Hanzo EVM Testnet Mining Contract
25    pub const HANZO_TESTNET_MINING: &str = "0x369100000000000000000000000000000000aAAI";
26    /// Zoo EVM Mining Contract
27    pub const ZOO_MAINNET_MINING: &str = "0x200200000000000000000000000000000000aAAI";
28    /// Zoo EVM Testnet Mining Contract
29    pub const ZOO_TESTNET_MINING: &str = "0x200201000000000000000000000000000000aAAI";
30    /// Lux C-Chain Mining Contract
31    pub const LUX_MAINNET_MINING: &str = "0x4C5558000000000000000000000000000000aAAI";
32    /// Lux C-Chain Testnet Mining Contract
33    pub const LUX_TESTNET_MINING: &str = "0x4C5559000000000000000000000000000000aAAI";
34
35    /// Teleport Protocol Contract - bridges AI coin to EVM chains
36    pub const TELEPORT_CONTRACT: &str = "0xAI00000000000000000000000000000TELEPORT";
37}
38
39/// Lux C-Chain configuration
40pub const LUX_MAINNET_CHAIN_ID: u64 = 96369;  // Lux C-Chain mainnet
41pub const LUX_TESTNET_CHAIN_ID: u64 = 96368;  // Lux C-Chain testnet
42
43/// EVM chain configuration
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ChainConfig {
46    pub chain_id: u64,
47    pub rpc_url: String,
48    pub mining_contract: String,
49    pub token_symbol: String,
50    pub token_decimals: u8,
51    pub block_time_ms: u64,
52}
53
54impl ChainConfig {
55    pub fn hanzo_mainnet() -> Self {
56        Self {
57            chain_id: 36963,  // Hanzo mainnet chain ID
58            rpc_url: "https://rpc.hanzo.network".to_string(),
59            mining_contract: contracts::HANZO_MAINNET_MINING.to_string(),
60            token_symbol: "HAI".to_string(),
61            token_decimals: 18,
62            block_time_ms: 2000,
63        }
64    }
65
66    pub fn hanzo_testnet() -> Self {
67        Self {
68            chain_id: 36964,  // Hanzo testnet chain ID
69            rpc_url: "https://rpc.hanzo-test.network".to_string(),
70            mining_contract: contracts::HANZO_TESTNET_MINING.to_string(),
71            token_symbol: "HAI".to_string(),
72            token_decimals: 18,
73            block_time_ms: 2000,
74        }
75    }
76
77    pub fn zoo_mainnet() -> Self {
78        Self {
79            chain_id: 200200,  // Zoo mainnet chain ID
80            rpc_url: "https://rpc.zoo.network".to_string(),
81            mining_contract: contracts::ZOO_MAINNET_MINING.to_string(),
82            token_symbol: "ZOO".to_string(),
83            token_decimals: 18,
84            block_time_ms: 2000,
85        }
86    }
87
88    pub fn zoo_testnet() -> Self {
89        Self {
90            chain_id: 200201,  // Zoo testnet chain ID
91            rpc_url: "https://rpc.zoo-test.network".to_string(),
92            mining_contract: contracts::ZOO_TESTNET_MINING.to_string(),
93            token_symbol: "ZOO".to_string(),
94            token_decimals: 18,
95            block_time_ms: 2000,
96        }
97    }
98
99    pub fn lux_mainnet() -> Self {
100        Self {
101            chain_id: LUX_MAINNET_CHAIN_ID,
102            rpc_url: "https://api.lux.network/ext/bc/C/rpc".to_string(),
103            mining_contract: contracts::LUX_MAINNET_MINING.to_string(),
104            token_symbol: "LUX".to_string(),
105            token_decimals: 18,
106            block_time_ms: 2000,
107        }
108    }
109
110    pub fn lux_testnet() -> Self {
111        Self {
112            chain_id: LUX_TESTNET_CHAIN_ID,
113            rpc_url: "https://api.lux-test.network/ext/bc/C/rpc".to_string(),
114            mining_contract: contracts::LUX_TESTNET_MINING.to_string(),
115            token_symbol: "LUX".to_string(),
116            token_decimals: 18,
117            block_time_ms: 2000,
118        }
119    }
120
121    pub fn from_network(network: &NetworkType) -> Self {
122        match network {
123            NetworkType::HanzoMainnet => Self::hanzo_mainnet(),
124            NetworkType::HanzoTestnet => Self::hanzo_testnet(),
125            NetworkType::ZooMainnet => Self::zoo_mainnet(),
126            NetworkType::ZooTestnet => Self::zoo_testnet(),
127            NetworkType::Custom(url) => Self {
128                chain_id: 0,
129                rpc_url: url.clone(),
130                mining_contract: String::new(),
131                token_symbol: "TOKEN".to_string(),
132                token_decimals: 18,
133                block_time_ms: 2000,
134            },
135        }
136    }
137}
138
139/// Miner registration on-chain
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct MinerRegistration {
142    /// Miner's wallet address
143    pub miner_address: String,
144    /// Node's P2P peer ID
145    pub peer_id: String,
146    /// GPU compute power (TFLOPS)
147    pub gpu_tflops: f32,
148    /// CPU compute power (GFLOPS)
149    pub cpu_gflops: f32,
150    /// Available VRAM (GB)
151    pub vram_gb: f32,
152    /// Available RAM (GB)
153    pub ram_gb: f32,
154    /// Network bandwidth (Mbps)
155    pub bandwidth_mbps: f32,
156    /// Supported AI capabilities
157    pub capabilities: Vec<MinerCapability>,
158    /// Registration timestamp
159    pub registered_at: u64,
160    /// Last heartbeat timestamp
161    pub last_heartbeat: u64,
162    /// Total jobs completed
163    pub jobs_completed: u64,
164    /// Reputation score (0-100)
165    pub reputation: u64,
166}
167
168/// AI compute capabilities a miner can offer
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170pub enum MinerCapability {
171    /// Text embeddings generation
172    Embedding,
173    /// Document reranking
174    Reranking,
175    /// LLM inference
176    Inference,
177    /// Model fine-tuning
178    Training,
179    /// GGUF quantization
180    Quantization,
181    /// Model storage/serving
182    Storage,
183    /// Custom compute
184    Custom(String),
185}
186
187/// How AI coins are earned in the protocol
188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189pub enum MiningRewardType {
190    /// Rewards for sharing training data
191    DataSharing {
192        dataset_id: String,
193        bytes_shared: u64,
194    },
195    /// Rewards for providing GPU/CPU compute
196    ComputeProvision {
197        job_id: String,
198        compute_units: u64,
199    },
200    /// Rewards for keeping models loaded/hosted
201    ModelHosting {
202        model_id: String,
203        hosting_hours: f64,
204    },
205    /// Rewards for specific model/embedding registration
206    ModelRegistration {
207        model_hash: String,
208        model_type: String,
209    },
210    /// Rewards for inference serving
211    InferenceServing {
212        model_id: String,
213        tokens_served: u64,
214    },
215}
216
217/// Destination chain for teleporting AI coins
218#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
219pub enum TeleportDestination {
220    /// Lux C-Chain (primary L1)
221    LuxCChain,
222    /// Zoo EVM
223    ZooEvm,
224    /// Hanzo EVM
225    HanzoEvm,
226}
227
228impl TeleportDestination {
229    pub fn chain_id(&self) -> u64 {
230        match self {
231            Self::LuxCChain => LUX_MAINNET_CHAIN_ID,
232            Self::ZooEvm => 200200,
233            Self::HanzoEvm => 36963,
234        }
235    }
236
237    pub fn rpc_url(&self) -> &str {
238        match self {
239            Self::LuxCChain => "https://api.lux.network/ext/bc/C/rpc",
240            Self::ZooEvm => "https://rpc.zoo.network",
241            Self::HanzoEvm => "https://rpc.hanzo.network",
242        }
243    }
244}
245
246/// Teleport transfer - bridging AI coins from protocol to EVM chains
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct TeleportTransfer {
249    /// Unique teleport ID
250    pub teleport_id: String,
251    /// Amount of AI coin to teleport (in wei)
252    pub amount: u128,
253    /// Source address (from AI protocol)
254    pub from_address: String,
255    /// Destination address (on EVM chain)
256    pub to_address: String,
257    /// Destination chain
258    pub destination: TeleportDestination,
259    /// Transfer status
260    pub status: TeleportStatus,
261    /// Protocol transaction hash
262    pub protocol_tx: Option<String>,
263    /// EVM transaction hash
264    pub evm_tx: Option<String>,
265    /// Timestamp initiated
266    pub initiated_at: u64,
267    /// Timestamp completed
268    pub completed_at: Option<u64>,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
272pub enum TeleportStatus {
273    /// Transfer initiated on AI protocol
274    Initiated,
275    /// Waiting for protocol confirmations
276    PendingConfirmation,
277    /// Transfer being processed by relayers
278    Processing,
279    /// Minting on destination chain
280    Minting,
281    /// Transfer complete
282    Completed,
283    /// Transfer failed
284    Failed(String),
285}
286
287/// Pending reward that can be claimed
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct PendingReward {
290    /// Job ID that generated this reward
291    pub job_id: String,
292    /// Amount in wei (1e18 = 1 token)
293    pub amount: u128,
294    /// Network where reward is claimable
295    pub network: NetworkType,
296    /// Block number when reward was allocated
297    pub block_number: u64,
298    /// Merkle proof for claiming (if applicable)
299    pub proof: Option<Vec<String>>,
300    /// Whether reward has been claimed
301    pub claimed: bool,
302}
303
304/// Cross-chain bridge transfer
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct BridgeTransfer {
307    /// Unique transfer ID
308    pub transfer_id: String,
309    /// Source chain
310    pub from_chain: NetworkType,
311    /// Destination chain
312    pub to_chain: NetworkType,
313    /// Amount being bridged
314    pub amount: u128,
315    /// Sender address
316    pub sender: String,
317    /// Recipient address
318    pub recipient: String,
319    /// Transfer status
320    pub status: BridgeStatus,
321    /// Source chain tx hash
322    pub source_tx: Option<String>,
323    /// Destination chain tx hash
324    pub dest_tx: Option<String>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
328pub enum BridgeStatus {
329    Pending,
330    SourceConfirmed,
331    Bridging,
332    DestConfirmed,
333    Completed,
334    Failed,
335}
336
337/// EVM client for interacting with Hanzo/Zoo chains
338pub struct EvmClient {
339    config: ChainConfig,
340    http_client: reqwest::Client,
341}
342
343impl EvmClient {
344    pub fn new(config: ChainConfig) -> Self {
345        Self {
346            config,
347            http_client: reqwest::Client::new(),
348        }
349    }
350
351    pub fn from_network(network: &NetworkType) -> Self {
352        Self::new(ChainConfig::from_network(network))
353    }
354
355    /// Get current block number
356    pub async fn get_block_number(&self) -> Result<u64, EvmError> {
357        let result = self.rpc_call("eth_blockNumber", serde_json::json!([])).await?;
358        let hex_str = result.as_str().ok_or(EvmError::InvalidResponse)?;
359        let block = u64::from_str_radix(hex_str.trim_start_matches("0x"), 16)
360            .map_err(|_| EvmError::InvalidResponse)?;
361        Ok(block)
362    }
363
364    /// Get native token balance
365    pub async fn get_balance(&self, address: &str) -> Result<u128, EvmError> {
366        let result = self.rpc_call(
367            "eth_getBalance",
368            serde_json::json!([address, "latest"]),
369        ).await?;
370        let hex_str = result.as_str().ok_or(EvmError::InvalidResponse)?;
371        let balance = u128::from_str_radix(hex_str.trim_start_matches("0x"), 16)
372            .map_err(|_| EvmError::InvalidResponse)?;
373        Ok(balance)
374    }
375
376    /// Get pending rewards for a miner
377    pub async fn get_pending_rewards(&self, miner_address: &str) -> Result<u128, EvmError> {
378        // Call pendingRewards(address) on mining contract
379        let data = encode_function_call("pendingRewards(address)", &[miner_address]);
380        let result = self.eth_call(&self.config.mining_contract, &data).await?;
381
382        let hex_str = result.as_str().ok_or(EvmError::InvalidResponse)?;
383        let rewards = u128::from_str_radix(hex_str.trim_start_matches("0x"), 16)
384            .unwrap_or(0);
385        Ok(rewards)
386    }
387
388    /// Register as a miner on the network
389    pub async fn register_miner(
390        &self,
391        private_key: &str,
392        stats: &PerformanceStats,
393        capabilities: &[MinerCapability],
394    ) -> Result<String, EvmError> {
395        // Encode registerMiner(uint256 gpuTflops, uint256 cpuGflops, uint256 vram, uint256 ram, bytes capabilities)
396        let caps_encoded = encode_capabilities(capabilities);
397        let data = encode_function_call(
398            "registerMiner(uint256,uint256,uint256,uint256,bytes)",
399            &[
400                &format!("{}", (stats.gpu_tflops * 1000.0) as u64),
401                &format!("{}", (stats.cpu_gflops * 1000.0) as u64),
402                &format!("{}", (stats.vram_gb * 1000.0) as u64),
403                &format!("{}", (stats.ram_gb * 1000.0) as u64),
404                &caps_encoded,
405            ],
406        );
407
408        self.send_transaction(private_key, &self.config.mining_contract, &data, 0).await
409    }
410
411    /// Claim pending rewards
412    pub async fn claim_rewards(&self, private_key: &str) -> Result<String, EvmError> {
413        let data = encode_function_call("claimRewards()", &[]);
414        self.send_transaction(private_key, &self.config.mining_contract, &data, 0).await
415    }
416
417    /// Submit job completion proof
418    pub async fn submit_job_completion(
419        &self,
420        private_key: &str,
421        job_id: &str,
422        result_hash: &str,
423    ) -> Result<String, EvmError> {
424        let data = encode_function_call(
425            "submitJobCompletion(bytes32,bytes32)",
426            &[job_id, result_hash],
427        );
428        self.send_transaction(private_key, &self.config.mining_contract, &data, 0).await
429    }
430
431    /// Send heartbeat to maintain miner registration
432    pub async fn send_heartbeat(&self, private_key: &str) -> Result<String, EvmError> {
433        let data = encode_function_call("heartbeat()", &[]);
434        self.send_transaction(private_key, &self.config.mining_contract, &data, 0).await
435    }
436
437    /// Initiate cross-chain bridge transfer
438    pub async fn bridge_tokens(
439        &self,
440        private_key: &str,
441        dest_chain: &NetworkType,
442        amount: u128,
443        recipient: &str,
444    ) -> Result<String, EvmError> {
445        let dest_chain_id = match dest_chain {
446            NetworkType::HanzoMainnet => 36963u64,
447            NetworkType::HanzoTestnet => 36964,
448            NetworkType::ZooMainnet => 200200,
449            NetworkType::ZooTestnet => 200201,
450            NetworkType::Custom(_) => return Err(EvmError::UnsupportedChain),
451        };
452
453        let data = encode_function_call(
454            "bridgeTokens(uint256,address,uint256)",
455            &[
456                &dest_chain_id.to_string(),
457                recipient,
458                &amount.to_string(),
459            ],
460        );
461        self.send_transaction(private_key, contracts::TELEPORT_CONTRACT, &data, amount).await
462    }
463
464    /// Internal: Make RPC call
465    async fn rpc_call(&self, method: &str, params: serde_json::Value) -> Result<serde_json::Value, EvmError> {
466        let request = serde_json::json!({
467            "jsonrpc": "2.0",
468            "method": method,
469            "params": params,
470            "id": 1
471        });
472
473        let response = self.http_client
474            .post(&self.config.rpc_url)
475            .json(&request)
476            .send()
477            .await
478            .map_err(|e| EvmError::RpcError(e.to_string()))?;
479
480        let json: serde_json::Value = response.json().await
481            .map_err(|e| EvmError::RpcError(e.to_string()))?;
482
483        if let Some(error) = json.get("error") {
484            return Err(EvmError::RpcError(error.to_string()));
485        }
486
487        json.get("result")
488            .cloned()
489            .ok_or(EvmError::InvalidResponse)
490    }
491
492    /// Internal: eth_call
493    async fn eth_call(&self, to: &str, data: &str) -> Result<serde_json::Value, EvmError> {
494        self.rpc_call(
495            "eth_call",
496            serde_json::json!([
497                {"to": to, "data": data},
498                "latest"
499            ]),
500        ).await
501    }
502
503    /// Internal: Send transaction
504    async fn send_transaction(
505        &self,
506        _private_key: &str,
507        to: &str,
508        data: &str,
509        value: u128,
510    ) -> Result<String, EvmError> {
511        // In production, this would:
512        // 1. Get nonce
513        // 2. Estimate gas
514        // 3. Build transaction
515        // 4. Sign with private key
516        // 5. Send via eth_sendRawTransaction
517
518        // For now, return placeholder
519        let _tx = serde_json::json!({
520            "to": to,
521            "data": data,
522            "value": format!("0x{:x}", value),
523            "chainId": format!("0x{:x}", self.config.chain_id),
524        });
525
526        // TODO: Implement actual transaction signing and sending
527        Ok(format!("0x{:064x}", rand::random::<u64>()))
528    }
529}
530
531/// Rewards manager for tracking and claiming rewards across chains
532pub struct RewardsManager {
533    /// Primary network for mining
534    primary_network: NetworkType,
535    /// EVM client for primary network
536    primary_client: EvmClient,
537    /// Optional secondary network (for cross-chain)
538    secondary_client: Option<EvmClient>,
539    /// Pending rewards
540    pending_rewards: Arc<RwLock<Vec<PendingReward>>>,
541    /// Total claimed rewards (in wei)
542    total_claimed: Arc<RwLock<u128>>,
543    /// Bridge transfers in progress
544    bridge_transfers: Arc<RwLock<Vec<BridgeTransfer>>>,
545}
546
547impl RewardsManager {
548    pub fn new(primary_network: NetworkType) -> Self {
549        let primary_client = EvmClient::from_network(&primary_network);
550
551        // Set up secondary client for cross-chain
552        let secondary_client = match &primary_network {
553            NetworkType::HanzoMainnet => Some(EvmClient::from_network(&NetworkType::ZooMainnet)),
554            NetworkType::HanzoTestnet => Some(EvmClient::from_network(&NetworkType::ZooTestnet)),
555            NetworkType::ZooMainnet => Some(EvmClient::from_network(&NetworkType::HanzoMainnet)),
556            NetworkType::ZooTestnet => Some(EvmClient::from_network(&NetworkType::HanzoTestnet)),
557            NetworkType::Custom(_) => None,
558        };
559
560        Self {
561            primary_network,
562            primary_client,
563            secondary_client,
564            pending_rewards: Arc::new(RwLock::new(Vec::new())),
565            total_claimed: Arc::new(RwLock::new(0)),
566            bridge_transfers: Arc::new(RwLock::new(Vec::new())),
567        }
568    }
569
570    /// Check and update pending rewards
571    pub async fn refresh_pending_rewards(&self, miner_address: &str) -> Result<u128, EvmError> {
572        let primary_rewards = self.primary_client.get_pending_rewards(miner_address).await?;
573
574        let secondary_rewards = if let Some(client) = &self.secondary_client {
575            client.get_pending_rewards(miner_address).await.unwrap_or(0)
576        } else {
577            0
578        };
579
580        Ok(primary_rewards + secondary_rewards)
581    }
582
583    /// Claim rewards from primary network
584    pub async fn claim_primary_rewards(&self, private_key: &str) -> Result<String, EvmError> {
585        let tx_hash = self.primary_client.claim_rewards(private_key).await?;
586        Ok(tx_hash)
587    }
588
589    /// Claim rewards from secondary network
590    pub async fn claim_secondary_rewards(&self, private_key: &str) -> Result<Option<String>, EvmError> {
591        if let Some(client) = &self.secondary_client {
592            let tx_hash = client.claim_rewards(private_key).await?;
593            Ok(Some(tx_hash))
594        } else {
595            Ok(None)
596        }
597    }
598
599    /// Claim rewards from all networks
600    pub async fn claim_all_rewards(&self, private_key: &str) -> Result<Vec<String>, EvmError> {
601        let mut tx_hashes = Vec::new();
602
603        // Claim primary
604        let primary_tx = self.primary_client.claim_rewards(private_key).await?;
605        tx_hashes.push(primary_tx);
606
607        // Claim secondary if available
608        if let Some(client) = &self.secondary_client {
609            if let Ok(tx) = client.claim_rewards(private_key).await {
610                tx_hashes.push(tx);
611            }
612        }
613
614        Ok(tx_hashes)
615    }
616
617    /// Bridge tokens from primary to secondary network
618    pub async fn bridge_to_secondary(
619        &self,
620        private_key: &str,
621        amount: u128,
622        recipient: &str,
623    ) -> Result<String, EvmError> {
624        let dest_chain = match &self.primary_network {
625            NetworkType::HanzoMainnet => NetworkType::ZooMainnet,
626            NetworkType::HanzoTestnet => NetworkType::ZooTestnet,
627            NetworkType::ZooMainnet => NetworkType::HanzoMainnet,
628            NetworkType::ZooTestnet => NetworkType::HanzoTestnet,
629            NetworkType::Custom(_) => return Err(EvmError::UnsupportedChain),
630        };
631
632        self.primary_client.bridge_tokens(private_key, &dest_chain, amount, recipient).await
633    }
634
635    /// Get total balance across all networks
636    pub async fn get_total_balance(&self, address: &str) -> Result<u128, EvmError> {
637        let primary_balance = self.primary_client.get_balance(address).await?;
638
639        let secondary_balance = if let Some(client) = &self.secondary_client {
640            client.get_balance(address).await.unwrap_or(0)
641        } else {
642            0
643        };
644
645        Ok(primary_balance + secondary_balance)
646    }
647
648    /// Get rewards summary
649    pub async fn get_rewards_summary(&self, address: &str) -> Result<RewardsSummary, EvmError> {
650        let primary_pending = self.primary_client.get_pending_rewards(address).await?;
651        let primary_balance = self.primary_client.get_balance(address).await?;
652
653        let (secondary_pending, secondary_balance) = if let Some(client) = &self.secondary_client {
654            let pending = client.get_pending_rewards(address).await.unwrap_or(0);
655            let balance = client.get_balance(address).await.unwrap_or(0);
656            (pending, balance)
657        } else {
658            (0, 0)
659        };
660
661        Ok(RewardsSummary {
662            primary_network: self.primary_network.clone(),
663            primary_pending,
664            primary_balance,
665            secondary_pending,
666            secondary_balance,
667            total_pending: primary_pending + secondary_pending,
668            total_balance: primary_balance + secondary_balance,
669            total_claimed: *self.total_claimed.read().await,
670        })
671    }
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
675pub struct RewardsSummary {
676    pub primary_network: NetworkType,
677    pub primary_pending: u128,
678    pub primary_balance: u128,
679    pub secondary_pending: u128,
680    pub secondary_balance: u128,
681    pub total_pending: u128,
682    pub total_balance: u128,
683    pub total_claimed: u128,
684}
685
686impl RewardsSummary {
687    /// Format amounts for display (convert wei to token units)
688    pub fn format_primary_pending(&self) -> f64 {
689        self.primary_pending as f64 / 1e18
690    }
691
692    pub fn format_total_balance(&self) -> f64 {
693        self.total_balance as f64 / 1e18
694    }
695}
696
697/// EVM-related errors
698#[derive(Debug, thiserror::Error)]
699pub enum EvmError {
700    #[error("RPC error: {0}")]
701    RpcError(String),
702    #[error("Invalid response from RPC")]
703    InvalidResponse,
704    #[error("Transaction failed: {0}")]
705    TransactionFailed(String),
706    #[error("Insufficient balance")]
707    InsufficientBalance,
708    #[error("Unsupported chain for this operation")]
709    UnsupportedChain,
710    #[error("Contract error: {0}")]
711    ContractError(String),
712}
713
714// Helper functions for ABI encoding (simplified)
715fn encode_function_call(signature: &str, _params: &[&str]) -> String {
716    // In production, use ethers-rs or alloy for proper ABI encoding
717    // For now, return a placeholder
718    let selector = keccak256_selector(signature);
719    format!("0x{}", selector)
720}
721
722fn keccak256_selector(signature: &str) -> String {
723    // Simplified: return first 8 hex chars of hash
724    // In production, use actual keccak256
725    let hash = blake3::hash(signature.as_bytes());
726    hex::encode(&hash.as_bytes()[..4])
727}
728
729fn encode_capabilities(capabilities: &[MinerCapability]) -> String {
730    // Encode capabilities as bytes
731    let encoded: Vec<u8> = capabilities.iter().map(|c| {
732        match c {
733            MinerCapability::Embedding => 0x01,
734            MinerCapability::Reranking => 0x02,
735            MinerCapability::Inference => 0x03,
736            MinerCapability::Training => 0x04,
737            MinerCapability::Quantization => 0x05,
738            MinerCapability::Storage => 0x06,
739            MinerCapability::Custom(_) => 0xFF,
740        }
741    }).collect();
742    hex::encode(&encoded)
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    #[test]
750    fn test_chain_configs() {
751        let hanzo = ChainConfig::hanzo_mainnet();
752        assert_eq!(hanzo.chain_id, 36963);
753        assert_eq!(hanzo.token_symbol, "HAI");
754
755        let zoo = ChainConfig::zoo_mainnet();
756        assert_eq!(zoo.chain_id, 200200);
757        assert_eq!(zoo.token_symbol, "ZOO");
758    }
759
760    #[test]
761    fn test_network_to_config() {
762        let config = ChainConfig::from_network(&NetworkType::HanzoMainnet);
763        assert_eq!(config.chain_id, 36963);
764
765        let config = ChainConfig::from_network(&NetworkType::ZooMainnet);
766        assert_eq!(config.chain_id, 200200);
767    }
768
769    #[test]
770    fn test_encode_capabilities() {
771        let caps = vec![
772            MinerCapability::Embedding,
773            MinerCapability::Inference,
774        ];
775        let encoded = encode_capabilities(&caps);
776        assert_eq!(encoded, "0103"); // 0x01 for Embedding, 0x03 for Inference
777    }
778
779    #[tokio::test]
780    async fn test_rewards_manager_creation() {
781        let manager = RewardsManager::new(NetworkType::HanzoMainnet);
782        assert!(manager.secondary_client.is_some());
783
784        let manager = RewardsManager::new(NetworkType::ZooMainnet);
785        assert!(manager.secondary_client.is_some());
786    }
787
788    #[test]
789    fn test_rewards_summary_formatting() {
790        let summary = RewardsSummary {
791            primary_network: NetworkType::HanzoMainnet,
792            primary_pending: 1_000_000_000_000_000_000, // 1 token
793            primary_balance: 10_000_000_000_000_000_000, // 10 tokens
794            secondary_pending: 500_000_000_000_000_000, // 0.5 tokens
795            secondary_balance: 5_000_000_000_000_000_000, // 5 tokens
796            total_pending: 1_500_000_000_000_000_000,
797            total_balance: 15_000_000_000_000_000_000,
798            total_claimed: 100_000_000_000_000_000_000,
799        };
800
801        assert!((summary.format_primary_pending() - 1.0).abs() < 0.001);
802        assert!((summary.format_total_balance() - 15.0).abs() < 0.001);
803    }
804}